You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@arrow.apache.org by "scovich (via GitHub)" <gi...@apache.org> on 2025/09/09 01:40:37 UTC

[PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

scovich opened a new pull request, #8299:
URL: https://github.com/apache/arrow-rs/pull/8299

   # Which issue does this PR close?
   
   We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax.
   
   - Closes #NNN.
   
   # Rationale for this change
   
   The original `cast_to_variant` code did columnar conversions of types to variant. For primitive types this worked ok, but for deeply nested types it means repeatedly creating new variants (and variant metadata), only to re-code them by copying the variant values to new arrays (with new metadata and field ids). Very expensive.
   
   # What changes are included in this PR?
   
   Follow the example of https://github.com/apache/arrow-rs/pull/8280, and introduce a row builder concept that takes individual array values and writes them to an `impl VariantBuilderExt`.
   
   # Are these changes tested?
   
   Existing unit tests continue to pass. Extensive new unit tests added as well.
   
   # Are there any user-facing changes?
   
   * `VariantBuilderExt` has a new `append_null` method.
   * `ObjectFieldBuilder` moved to `builder.rs` and made public


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "klion26 (via GitHub)" <gi...@apache.org>.
klion26 commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2347785006


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,1995 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    /// Appends a single row at the given index to the supplied builder.
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        use ArrowToVariantRowBuilder::*;
+        match self {
+            Null(b) => b.append_row(builder, index),
+            Boolean(b) => b.append_row(builder, index),
+            PrimitiveInt8(b) => b.append_row(builder, index),
+            PrimitiveInt16(b) => b.append_row(builder, index),
+            PrimitiveInt32(b) => b.append_row(builder, index),
+            PrimitiveInt64(b) => b.append_row(builder, index),
+            PrimitiveUInt8(b) => b.append_row(builder, index),
+            PrimitiveUInt16(b) => b.append_row(builder, index),
+            PrimitiveUInt32(b) => b.append_row(builder, index),
+            PrimitiveUInt64(b) => b.append_row(builder, index),
+            PrimitiveFloat16(b) => b.append_row(builder, index),
+            PrimitiveFloat32(b) => b.append_row(builder, index),
+            PrimitiveFloat64(b) => b.append_row(builder, index),
+            Decimal32(b) => b.append_row(builder, index),
+            Decimal64(b) => b.append_row(builder, index),
+            Decimal128(b) => b.append_row(builder, index),
+            Decimal256(b) => b.append_row(builder, index),
+            TimestampSecond(b) => b.append_row(builder, index),
+            TimestampMillisecond(b) => b.append_row(builder, index),
+            TimestampMicrosecond(b) => b.append_row(builder, index),
+            TimestampNanosecond(b) => b.append_row(builder, index),
+            Date32(b) => b.append_row(builder, index),
+            Date64(b) => b.append_row(builder, index),
+            Time32Second(b) => b.append_row(builder, index),
+            Time32Millisecond(b) => b.append_row(builder, index),
+            Time64Microsecond(b) => b.append_row(builder, index),
+            Time64Nanosecond(b) => b.append_row(builder, index),
+            Binary(b) => b.append_row(builder, index),
+            LargeBinary(b) => b.append_row(builder, index),
+            BinaryView(b) => b.append_row(builder, index),
+            FixedSizeBinary(b) => b.append_row(builder, index),
+            Utf8(b) => b.append_row(builder, index),
+            LargeUtf8(b) => b.append_row(builder, index),
+            Utf8View(b) => b.append_row(builder, index),
+            List(b) => b.append_row(builder, index),
+            LargeList(b) => b.append_row(builder, index),
+            Struct(b) => b.append_row(builder, index),
+            Map(b) => b.append_row(builder, index),
+            Union(b) => b.append_row(builder, index),
+            Dictionary(b) => b.append_row(builder, index),
+            RunEndEncodedInt16(b) => b.append_row(builder, index),
+            RunEndEncodedInt32(b) => b.append_row(builder, index),
+            RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+    options: &'a CastOptions,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    use ArrowToVariantRowBuilder::*;
+    let builder =
+        match data_type {
+            DataType::Null => Null(NullArrowToVariantBuilder),
+            DataType::Boolean => Boolean(BooleanArrowToVariantBuilder::new(array)),
+            DataType::Int8 => PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int16 => PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int32 => PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int64 => PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt8 => PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt16 => PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt32 => PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt64 => PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float16 => PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float32 => PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float64 => PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Decimal32(_, scale) => {
+                Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal64(_, scale) => {
+                Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal128(_, scale) => {
+                Decimal128(Decimal128ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal256(_, scale) => {
+                Decimal256(Decimal256ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Timestamp(time_unit, time_zone) => {
+                match time_unit {
+                    TimeUnit::Second => TimestampSecond(TimestampArrowToVariantBuilder::new(
+                        array,
+                        options,
+                        time_zone.is_some(),
+                    )),
+                    TimeUnit::Millisecond => TimestampMillisecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                    TimeUnit::Microsecond => TimestampMicrosecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                    TimeUnit::Nanosecond => TimestampNanosecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                }
+            }
+            DataType::Date32 => Date32(DateArrowToVariantBuilder::new(array, options)),
+            DataType::Date64 => Date64(DateArrowToVariantBuilder::new(array, options)),
+            DataType::Time32(time_unit) => match time_unit {
+                TimeUnit::Second => Time32Second(TimeArrowToVariantBuilder::new(array, options)),
+                TimeUnit::Millisecond => {
+                    Time32Millisecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported Time32 unit: {time_unit:?}"
+                    )))
+                }
+            },
+            DataType::Time64(time_unit) => match time_unit {
+                TimeUnit::Microsecond => {
+                    Time64Microsecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                TimeUnit::Nanosecond => {
+                    Time64Nanosecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported Time64 unit: {time_unit:?}"
+                    )))
+                }
+            },
+            DataType::Duration(_) | DataType::Interval(_) => {
+                return Err(ArrowError::InvalidArgumentError(
+                    "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                        .to_string(),
+                ))
+            }
+            DataType::Binary => Binary(BinaryArrowToVariantBuilder::new(array)),
+            DataType::LargeBinary => LargeBinary(BinaryArrowToVariantBuilder::new(array)),
+            DataType::BinaryView => BinaryView(BinaryViewArrowToVariantBuilder::new(array)),
+            DataType::FixedSizeBinary(_) => {
+                FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder::new(array))
+            }
+            DataType::Utf8 => Utf8(StringArrowToVariantBuilder::new(array)),
+            DataType::LargeUtf8 => LargeUtf8(StringArrowToVariantBuilder::new(array)),
+            DataType::Utf8View => Utf8View(StringViewArrowToVariantBuilder::new(array)),
+            DataType::List(_) => List(ListArrowToVariantBuilder::new(array, options)?),
+            DataType::LargeList(_) => LargeList(ListArrowToVariantBuilder::new(array, options)?),
+            DataType::Struct(_) => Struct(StructArrowToVariantBuilder::new(
+                array.as_struct(),
+                options,
+            )?),
+            DataType::Map(_, _) => Map(MapArrowToVariantBuilder::new(array, options)?),
+            DataType::Union(_, _) => Union(UnionArrowToVariantBuilder::new(array, options)?),
+            DataType::Dictionary(_, _) => {
+                Dictionary(DictionaryArrowToVariantBuilder::new(array, options)?)
+            }
+            DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+                DataType::Int16 => {
+                    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                DataType::Int32 => {
+                    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                DataType::Int64 => {
+                    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported run ends type: {:?}",
+                        run_ends.data_type()
+                    )));
+                }
+            },
+            dt => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported data type for casting to Variant: {dt:?}",
+                )));
+            }
+        };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+///
+/// The macro optionally allows to define a transform for values read from the underlying
+/// array. Transforms of the form `|value| { ... }` are infallible (and should produce something
+/// that implements `Into<Variant>`), while transforms of the form `|value| -> Option<_> { ... }`
+/// are fallible (and should produce `Option<impl Into<Variant>>`); a failed tarnsform will either
+/// append null to the builder or return an error, depending on cast options.
+///
+/// Also supports optional extra fields that are passed to the constructor and which are available
+/// by reference in the value transform. Providing a fallible value transform requires also
+/// providing the extra field `options: &'a CastOptions`.
+// TODO: If/when the macro_metavar_expr feature stabilizes, the `ignore` meta-function would allow
+// us to "use" captured tokens without emitting them:
+//
+// ```
+// $(
+//     ${ignore($value)}
+//     $(
+//         ${ignore($option_ty)}
+//         options: &$lifetime CastOptions,
+//     )?
+// )?
+// ```
+//
+// That, in turn, would allow us to inject the `options` field whenever the user specifies a
+// fallible value transform, instead of requiring them to manually define it. This might not be
+// worth the trouble, tho, because it makes for some pretty bulky and unwieldy macro expansions.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
+        $( where $where_path:path: $where_bound:path $(,)? )?
+        $({ $($field:ident: $field_type:ty),+ $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr }
+        $(, |$value:ident| $(-> Option<$option_ty:ty>)? $value_transform:expr)?
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $bound )?>
+        $( where $where_path: $where_bound )?
+        {
+            array: &$lifetime $array_type,
+            $( $( $field: $field_type, )+ )?
+        }
+
+        impl<$lifetime $(, $generic: $bound+ )?> $name<$lifetime $(, $generic)?>
+        $( where $where_path: $where_bound )?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $( $field: $field_type ),+ )?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $( $( $field, )+ )?
+                }
+            }
+
+            fn append_row(&self, builder: &mut impl VariantBuilderExt, index: usize) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    // Macro hygiene: Give any extra fields names the value transform can access.
+                    //
+                    // The value transform doesn't normally reference cast options, but the macro's
+                    // caller still has to declare the field because stable rust has no way to "use"
+                    // a captured token without emitting it. So, silence unused variable warnings,
+                    // assuming that's the `options` field. Unfortunately, that also silences
+                    // legitimate compiler warnings if an infallible value transform fails to use
+                    // its first extra field.
+                    $(
+                        #[allow(unused)]
+                        $( let $field = &self.$field; )+
+                    )?
+
+                    // Apply the value transform, if any (with name swapping for hygiene)
+                    let value = self.array.value(index);
+                    $(
+                        let $value = value;
+                        let value = $value_transform;
+                        $(
+                            // NOTE: The `?` macro expansion fails without the type annotation.
+                            let Some(value): Option<$option_ty> = value else {
+                                if self.options.strict {
+                                    return Err(ArrowError::ComputeError(format!(
+                                        "Failed to convert value at index {index}: conversion failed",
+                                    )));
+                                } else {
+                                    builder.append_null();
+                                    return Ok(());
+                                }
+                            };
+                        )?
+                    )?
+                    builder.append_value(value);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() }
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() }
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        options: &'a CastOptions,
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        // Convert using Arrow's temporal conversion functions
+        as_datetime::<T>(value).map(|naive_datetime| {
+            if *has_time_zone {
+                // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+                let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+                Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+            } else {
+                // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+                Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+            }
+        })
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() }
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() }
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() }
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() }
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() }
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        _index: usize,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(&mut list_builder, value_index)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(
+        struct_array: &'a arrow::array::StructArray,
+        options: &'a CastOptions,
+    ) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder = make_arrow_to_variant_row_builder(
+                field_array.data_type(),
+                field_array.as_ref(),
+                options,
+            )?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(&mut field_builder, index)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(&mut field_builder, kv_index)?;
+        }
+
+        object_builder.finish();
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+///
+/// NOTE: Union type ids are _not_ required to be dense, hence the hash map for child builders.
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder = make_arrow_to_variant_row_builder(
+                child_array.data_type(),
+                child_array.as_ref(),
+                options,
+            )?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(builder, value_offset)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(builder, normalized_key)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(builder, self.run_number)?;
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{VariantArray, VariantArrayBuilder};
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    /// Builds a VariantArray from an Arrow array using the row builder.
+    fn execute_row_builder_test(array: &dyn Array) -> VariantArray {
+        let options = CastOptions::default();
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(array.data_type(), array, &options).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(array.len());
+
+        // The repetitive loop that appears in every test
+        for i in 0..array.len() {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(&mut variant_builder, i).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), array.len());
+        variant_array
+    }
+
+    /// Generic helper function to test row builders with basic assertion patterns.
+    /// Uses execute_row_builder_test and adds simple value comparison assertions.
+    fn test_row_builder_basic(array: &dyn Array, expected_values: Vec<Option<Variant>>) {
+        let variant_array = execute_row_builder_test(array);
+
+        // The repetitive assertion pattern
+        for (i, expected) in expected_values.iter().enumerate() {
+            match expected {
+                Some(variant) => {
+                    assert_eq!(variant_array.value(i), *variant, "Mismatch at index {}", i)
+                }
+                None => assert!(variant_array.is_null(i), "Expected null at index {}", i),
+            }
+        }
+    }
+
+    #[test]
+    fn test_primitive_row_builder() {

Review Comment:
   Very nice tests! it's very help to understand the variant logic and the respective arrow type.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2334742498


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2500 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(

Review Comment:
   is there any reason in the future this couldn't be vectorized? 
   
   Something like
   ```rust
       pub fn append_all_rows(        &mut self,
           builder: &mut impl VariantBuilderExt,
       
   ...
   ```
   
   as written here it is going to have a per-row type dispatch
   
   I think we could do it as a follow on PR potentially if needed



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2500 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+    options: &'a CastOptions,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => {
+            ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Date64 => {
+            ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::Time32Second(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::Time32Millisecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::Time64Microsecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::Time64Nanosecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => {
+            ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Struct(_) => ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(
+            array.as_struct(),
+            options,
+        )?),
+        DataType::Map(_, _) => {
+            ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Dictionary(_, _) => ArrowToVariantRowBuilder::Dictionary(
+            DictionaryArrowToVariantBuilder::new(array, options)?,
+        ),
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+///
+/// The macro optionally allows to define a transform for values read from the underlying
+/// array. Transforms of the form `|value| { ... }` are infallible (and should produce something
+/// that implements `Into<Variant>`), while transforms of the form `|value| -> Option<_> { ... }`
+/// are fallible (and should produce `Option<impl Into<Variant>>`); a failed tarnsform will either
+/// append null to the builder or return an error, depending on cast options.
+///
+/// Also supports optional extra fields that are passed to the constructor and which are available
+/// by reference in the value transform. Providing a fallible value transform requires also
+/// providing the extra field `options: &'a CastOptions`.
+// TODO: If/when the macro_metavar_expr feature stabilizes, the `ignore` meta-function would allow
+// us to "use" captured tokens without emitting them:
+//
+// ```
+// $(
+//     ${ignore($value)}
+//     $(
+//         ${ignore($option_ty)}
+//         options: &$lifetime CastOptions,
+//     )?
+// )?
+// ```
+//
+// That, in turn, would allow us to inject the `options` field whenever the user specifies a
+// fallible value transform, instead of requiring them to manually define it. This might not be
+// worth the trouble, tho, because it makes for some pretty bulky and unwieldy macro expansions.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
+        $( where $where_path:path: $where_bound:path $(,)? )?
+        $({ $($field:ident: $field_type:ty),+ $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr }
+        $(, |$value:ident| $(-> Option<$option_ty:ty>)? $value_transform:expr)?
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $bound )?>
+        $( where $where_path: $where_bound )?
+        {
+            array: &$lifetime $array_type,
+            $( $( $field: $field_type, )+ )?
+        }
+
+        impl<$lifetime $(, $generic: $bound+ )?> $name<$lifetime $(, $generic)?>
+        $( where $where_path: $where_bound )?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $( $field: $field_type ),+ )?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $( $( $field, )+ )?
+                }
+            }
+
+            fn append_row(&self, builder: &mut impl VariantBuilderExt, index: usize) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    // Macro hygiene: Give any extra fields names the value transform can access.
+                    //
+                    // The value transform doesn't normally reference cast options, but the macro's
+                    // caller still has to declare the field because stable rust has no way to "use"
+                    // a captured token without emitting it. So, silence unused variable warnings,
+                    // assuming that's the `options` field. Unfortunately, that also silences
+                    // legitimate compiler warnings if an infallible value transform fails to use
+                    // its first extra field.
+                    $(
+                        #[allow(unused)]
+                        $( let $field = &self.$field; )+
+                    )?
+
+                    // Apply the value transform, if any (with name swapping for hygiene)
+                    let value = self.array.value(index);
+                    $(
+                        let $value = value;
+                        let value = $value_transform;
+                        $(
+                            // NOTE: The `?` macro expansion fails without the type annotation.
+                            let Some(value): Option<$option_ty> = value else {
+                                if self.options.strict {
+                                    return Err(ArrowError::ComputeError(format!(
+                                        "Failed to convert value at index {index}: conversion failed",
+                                    )));
+                                } else {
+                                    builder.append_null();
+                                    return Ok(());
+                                }
+                            };
+                        )?
+                    )?
+                    builder.append_value(value);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() }
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() }
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        options: &'a CastOptions,
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        // Convert using Arrow's temporal conversion functions
+        as_datetime::<T>(value).map(|naive_datetime| {
+            if *has_time_zone {
+                // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+                let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+                Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+            } else {
+                // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+                Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+            }
+        })
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() }
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() }
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() }
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() }
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() }
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        _index: usize,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(&mut list_builder, value_index)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(
+        struct_array: &'a arrow::array::StructArray,
+        options: &'a CastOptions,
+    ) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder = make_arrow_to_variant_row_builder(
+                field_array.data_type(),
+                field_array.as_ref(),
+                options,
+            )?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(&mut field_builder, index)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(&mut field_builder, kv_index)?;
+        }
+
+        object_builder.finish();
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+///
+/// NOTE: Union type ids are _not_ required to be dense, hence the hash map for child builders.
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder = make_arrow_to_variant_row_builder(
+                child_array.data_type(),
+                child_array.as_ref(),
+                options,
+            )?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(builder, value_offset)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(builder, normalized_key)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(builder, self.run_number)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::VariantArrayBuilder;
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    #[test]
+    fn test_primitive_row_builder() {
+        // Test Int32Array
+        let int_array = Int32Array::from(vec![Some(42), None, Some(100)]);
+        let options = CastOptions::default();
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(int_array.data_type(), &int_array, &options).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 0).unwrap();
+        variant_builder.finish();
+
+        // Test null value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 1).unwrap();
+        variant_builder.finish();
+
+        // Test second value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 2).unwrap();
+        variant_builder.finish();

Review Comment:
   This code segment is repeated many times. Maybe we could make a function like this
   
   ```rust
   fn copy_all(input_array: &dyn Array) -> VariantArray {
     for i in 0..input_array.len() {
           let mut variant_builder = array_builder.variant_builder();
           row_builder.append_row(&mut variant_builder, 1).unwrap();
           variant_builder.finish();
      }
   }
   ```
   
   A more ideal version in my mind would convert the entire Array (not just a row at a time). Something like this (untested). Subsets of rows could be handled via `slice()`ing the Array
   
   ```rust
   // make conevrter
           let mut row_builder =
               make_arrow_to_variant_row_builder(int_array.data_type(), &int_array, &options).unwrap();
   // create output variant array
          let mut variant_builder = array_builder.variant_builder();
   // convert all rows from the array        
           row_builder.convert(&mut array_builder).unwrap();
   ```
   
   



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2500 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(

Review Comment:
   😮



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2500 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+    options: &'a CastOptions,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => {
+            ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Date64 => {
+            ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::Time32Second(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::Time32Millisecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::Time64Microsecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::Time64Nanosecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => {
+            ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Struct(_) => ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(
+            array.as_struct(),
+            options,
+        )?),
+        DataType::Map(_, _) => {
+            ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Dictionary(_, _) => ArrowToVariantRowBuilder::Dictionary(
+            DictionaryArrowToVariantBuilder::new(array, options)?,
+        ),
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+///
+/// The macro optionally allows to define a transform for values read from the underlying
+/// array. Transforms of the form `|value| { ... }` are infallible (and should produce something
+/// that implements `Into<Variant>`), while transforms of the form `|value| -> Option<_> { ... }`
+/// are fallible (and should produce `Option<impl Into<Variant>>`); a failed tarnsform will either
+/// append null to the builder or return an error, depending on cast options.
+///
+/// Also supports optional extra fields that are passed to the constructor and which are available
+/// by reference in the value transform. Providing a fallible value transform requires also
+/// providing the extra field `options: &'a CastOptions`.
+// TODO: If/when the macro_metavar_expr feature stabilizes, the `ignore` meta-function would allow
+// us to "use" captured tokens without emitting them:
+//
+// ```
+// $(
+//     ${ignore($value)}
+//     $(
+//         ${ignore($option_ty)}
+//         options: &$lifetime CastOptions,
+//     )?
+// )?
+// ```
+//
+// That, in turn, would allow us to inject the `options` field whenever the user specifies a
+// fallible value transform, instead of requiring them to manually define it. This might not be
+// worth the trouble, tho, because it makes for some pretty bulky and unwieldy macro expansions.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
+        $( where $where_path:path: $where_bound:path $(,)? )?
+        $({ $($field:ident: $field_type:ty),+ $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr }
+        $(, |$value:ident| $(-> Option<$option_ty:ty>)? $value_transform:expr)?
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $bound )?>
+        $( where $where_path: $where_bound )?
+        {
+            array: &$lifetime $array_type,
+            $( $( $field: $field_type, )+ )?
+        }
+
+        impl<$lifetime $(, $generic: $bound+ )?> $name<$lifetime $(, $generic)?>
+        $( where $where_path: $where_bound )?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $( $field: $field_type ),+ )?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $( $( $field, )+ )?
+                }
+            }
+
+            fn append_row(&self, builder: &mut impl VariantBuilderExt, index: usize) -> Result<(), ArrowError> {

Review Comment:
   as mentioned above, it might eventually be a good idea to make a vectorized version of this method (that gets given something that can create VariantBuilderExt) but let's get this working first and then optimize later if/when needed



##########
parquet-variant-compute/src/variant_array_builder.rs:
##########
@@ -199,9 +199,14 @@ pub struct VariantArrayVariantBuilder<'a> {
     metadata_offsets: &'a mut Vec<usize>,
     value_offsets: &'a mut Vec<usize>,
     nulls: &'a mut NullBufferBuilder,
+    is_null: bool,
 }
 
 impl VariantBuilderExt for VariantArrayVariantBuilder<'_> {
+    /// Appending NULL to a variant array produces an actual NULL value
+    fn append_null(&mut self) {

Review Comment:
   This seems like it will overwrite any value that is currently set -- so if someone did
   
   ```rust
   builder.append_value(..);
   builder.append_null(); // will wipe out the value ??
   ```
   
   I am not sure this is a problem or not, I just wanted to confirm my understanding



##########
parquet-variant-compute/src/cast_to_variant.rs:
##########
@@ -178,489 +57,34 @@ pub fn cast_to_variant_with_options(
     input: &dyn Array,
     options: &CastOptions,
 ) -> Result<VariantArray, ArrowError> {
-    let mut builder = VariantArrayBuilder::new(input.len());
-
-    let input_type = input.data_type();
-    match input_type {
-        DataType::Null => {
-            for _ in 0..input.len() {
-                builder.append_null();
-            }
-        }
-        DataType::Boolean => {
-            non_generic_conversion_array!(input.as_boolean(), |v| v, builder);
-        }
-        DataType::Int8 => {
-            primitive_conversion_array!(Int8Type, input, builder);
-        }
-        DataType::Int16 => {
-            primitive_conversion_array!(Int16Type, input, builder);
-        }
-        DataType::Int32 => {
-            primitive_conversion_array!(Int32Type, input, builder);
-        }
-        DataType::Int64 => {
-            primitive_conversion_array!(Int64Type, input, builder);
-        }
-        DataType::UInt8 => {
-            primitive_conversion_array!(UInt8Type, input, builder);
-        }
-        DataType::UInt16 => {
-            primitive_conversion_array!(UInt16Type, input, builder);
-        }
-        DataType::UInt32 => {
-            primitive_conversion_array!(UInt32Type, input, builder);
-        }
-        DataType::UInt64 => {
-            primitive_conversion_array!(UInt64Type, input, builder);
-        }
-        DataType::Float16 => {
-            generic_conversion_array!(Float16Type, as_primitive, f32::from, input, builder);
-        }
-        DataType::Float32 => {
-            primitive_conversion_array!(Float32Type, input, builder);
-        }
-        DataType::Float64 => {
-            primitive_conversion_array!(Float64Type, input, builder);
-        }
-        DataType::Decimal32(_, scale) => {
-            generic_conversion_array!(
-                Decimal32Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i32, VariantDecimal4),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal64(_, scale) => {
-            generic_conversion_array!(
-                Decimal64Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i64, VariantDecimal8),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal128(_, scale) => {
-            generic_conversion_array!(
-                Decimal128Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i128, VariantDecimal16),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal256(_, scale) => {
-            generic_conversion_array!(
-                Decimal256Type,
-                as_primitive,
-                |v: i256| {
-                    // Since `i128::MAX` is larger than the max value of `VariantDecimal16`,
-                    // any `i256` value that cannot be cast to `i128` is unable to be cast to `VariantDecimal16` either.
-                    // Therefore, we can safely convert `i256` to `i128` first and process it like `i128`.
-                    if let Some(v) = v.to_i128() {
-                        decimal_to_variant_decimal!(v, scale, i128, VariantDecimal16)
-                    } else {
-                        Variant::Null
-                    }
-                },
-                input,
-                builder
-            );
-        }
-        DataType::Timestamp(time_unit, time_zone) => {
-            convert_timestamp_with_options(time_unit, time_zone, input, &mut builder, options)?;
-        }
-        DataType::Time32(unit) => {
-            match *unit {
-                TimeUnit::Second => {
-                    generic_conversion_array!(
-                        Time32SecondType,
-                        as_primitive,
-                        // nano second are always 0
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(v as u32, 0u32),
-                        input,
-                        builder,
-                        options.strict
-                    )?;
-                }
-                TimeUnit::Millisecond => {
-                    generic_conversion_array!(
-                        Time32MillisecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            v as u32 / 1000,
-                            (v as u32 % 1000) * 1_000_000
-                        ),
-                        input,
-                        builder,
-                        options.strict
-                    )?;
-                }
-                _ => {
-                    return Err(ArrowError::CastError(format!(
-                        "Unsupported Time32 unit: {:?}",
-                        unit
-                    )));
-                }
-            };
-        }
-        DataType::Time64(unit) => {
-            match *unit {
-                TimeUnit::Microsecond => {
-                    generic_conversion_array!(
-                        Time64MicrosecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            (v / 1_000_000) as u32,
-                            (v % 1_000_000 * 1_000) as u32
-                        ),
-                        input,
-                        builder,
-                        options.strict
-                    )?;
-                }
-                TimeUnit::Nanosecond => {
-                    generic_conversion_array!(
-                        Time64NanosecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            (v / 1_000_000_000) as u32,
-                            (v % 1_000_000_000) as u32
-                        ),
-                        input,
-                        builder,
-                        options.strict
-                    )?;
-                }
-                _ => {
-                    return Err(ArrowError::CastError(format!(
-                        "Unsupported Time64 unit: {:?}",
-                        unit
-                    )));
-                }
-            };
-        }
-        DataType::Duration(_) | DataType::Interval(_) => {
-            return Err(ArrowError::InvalidArgumentError(
-                "Casting duration/interval types to Variant is not supported. \
-                 The Variant format does not define duration/interval types."
-                    .to_string(),
-            ));
-        }
-        DataType::Binary => {
-            generic_conversion_array!(BinaryType, as_bytes, |v| v, input, builder);
-        }
-        DataType::LargeBinary => {
-            generic_conversion_array!(LargeBinaryType, as_bytes, |v| v, input, builder);
-        }
-        DataType::BinaryView => {
-            generic_conversion_array!(BinaryViewType, as_byte_view, |v| v, input, builder);
-        }
-        DataType::FixedSizeBinary(_) => {
-            non_generic_conversion_array!(input.as_fixed_size_binary(), |v| v, builder);
-        }
-        DataType::Utf8 => {
-            generic_conversion_array!(i32, as_string, |v| v, input, builder);
-        }
-        DataType::LargeUtf8 => {
-            generic_conversion_array!(i64, as_string, |v| v, input, builder);
-        }
-        DataType::Utf8View => {
-            non_generic_conversion_array!(input.as_string_view(), |v| v, builder);
-        }
-        DataType::Date32 => {
-            generic_conversion_array!(
-                Date32Type,
-                as_primitive,
-                |v: i32| -> NaiveDate { Date32Type::to_naive_date(v) },
-                input,
-                builder
-            );
-        }
-        DataType::Date64 => {
-            generic_conversion_array!(
-                Date64Type,
-                as_primitive,
-                |v: i64| Date64Type::to_naive_date_opt(v),
-                input,
-                builder,
-                options.strict
-            )?;
-        }
-        DataType::List(_) => convert_list::<i32>(input, &mut builder)?,
-        DataType::LargeList(_) => convert_list::<i64>(input, &mut builder)?,
-        DataType::Struct(_) => convert_struct(input, &mut builder)?,
-        DataType::Map(field, _) => convert_map(field, input, &mut builder)?,
-        DataType::Union(fields, _) => convert_union(fields, input, &mut builder)?,
-        DataType::Dictionary(_, _) => convert_dictionary_encoded(input, &mut builder)?,
-        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
-            DataType::Int16 => convert_run_end_encoded::<Int16Type>(input, &mut builder)?,
-            DataType::Int32 => convert_run_end_encoded::<Int32Type>(input, &mut builder)?,
-            DataType::Int64 => convert_run_end_encoded::<Int64Type>(input, &mut builder)?,
-            _ => {
-                return Err(ArrowError::CastError(format!(
-                    "Unsupported run ends type: {:?}",
-                    run_ends.data_type()
-                )));
-            }
-        },
-        dt => {
-            return Err(ArrowError::CastError(format!(
-                "Unsupported data type for casting to Variant: {dt:?}",
-            )));
-        }
-    };
-    Ok(builder.build())
-}
-
-/// Generic function to convert list arrays (both List and LargeList) to variant arrays
-fn convert_list<O: OffsetSizeTrait>(
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    let list_array = input.as_list::<O>();
-    let values = list_array.values();
-    let offsets = list_array.offsets();
-
-    let first_offset = *offsets.first().expect("There should be an offset");
-    let length = *offsets.last().expect("There should be an offset") - first_offset;
-    let sliced_values = values.slice(first_offset.as_usize(), length.as_usize());
-
-    let values_variant_array = cast_to_variant(sliced_values.as_ref())?;
-    let new_offsets = OffsetBuffer::new(ScalarBuffer::from_iter(
-        offsets.iter().map(|o| *o - first_offset),
-    ));
-
-    for i in 0..list_array.len() {
-        if list_array.is_null(i) {
-            builder.append_null();
-            continue;
-        }
-
-        let start = new_offsets[i].as_usize();
-        let end = new_offsets[i + 1].as_usize();
-
-        // Start building the inner VariantList
-        let mut variant_builder = VariantBuilder::new();
-        let mut list_builder = variant_builder.new_list();
-
-        // Add all values from the slice
-        for j in start..end {
-            list_builder.append_value(values_variant_array.value(j));
-        }
-
-        list_builder.finish();
-
-        let (metadata, value) = variant_builder.finish();
-        let variant = Variant::new(&metadata, &value);
-        builder.append_variant(variant)
-    }
-
-    Ok(())
-}
-
-fn convert_struct(input: &dyn Array, builder: &mut VariantArrayBuilder) -> Result<(), ArrowError> {
-    let struct_array = input.as_struct();
-
-    // Pre-convert all field arrays once for better performance
-    // This avoids converting the same field array multiple times
-    // Alternative approach: Use slicing per row: field_array.slice(i, 1)
-    // However, pre-conversion is more efficient for typical use cases
-    let field_variant_arrays: Result<Vec<_>, _> = struct_array
-        .columns()
-        .iter()
-        .map(|field_array| cast_to_variant(field_array.as_ref()))
-        .collect();
-    let field_variant_arrays = field_variant_arrays?;
-
-    // Cache column names to avoid repeated calls
-    let column_names = struct_array.column_names();
-
-    for i in 0..struct_array.len() {
-        if struct_array.is_null(i) {
-            builder.append_null();
-            continue;
-        }
-
-        // Create a VariantBuilder for this struct instance
-        let mut variant_builder = VariantBuilder::new();
-        let mut object_builder = variant_builder.new_object();
-
-        // Iterate through all fields in the struct
-        for (field_idx, field_name) in column_names.iter().enumerate() {
-            // Use pre-converted field variant arrays for better performance
-            // Check nulls directly from the pre-converted arrays instead of accessing column again
-            if !field_variant_arrays[field_idx].is_null(i) {
-                let field_variant = field_variant_arrays[field_idx].value(i);
-                object_builder.insert(field_name, field_variant);
-            }
-            // Note: we skip null fields rather than inserting Variant::Null
-            // to match Arrow struct semantics where null fields are omitted
-        }
-
-        object_builder.finish();
-        let (metadata, value) = variant_builder.finish();
-        let variant = Variant::try_new(&metadata, &value)?;
-        builder.append_variant(variant);
-    }
-
-    Ok(())
-}
-
-fn convert_map(
-    field: &FieldRef,
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    match field.data_type() {
-        DataType::Struct(_) => {
-            let map_array = input.as_map();
-            let keys = cast(map_array.keys(), &DataType::Utf8)?;
-            let key_strings = keys.as_string::<i32>();
-            let values = cast_to_variant(map_array.values())?;
-            let offsets = map_array.offsets();
-
-            let mut start_offset = offsets[0];
-            for end_offset in offsets.iter().skip(1) {
-                if start_offset >= *end_offset {
-                    builder.append_null();
-                    continue;
-                }
-
-                let length = (end_offset - start_offset) as usize;
-
-                let mut variant_builder = VariantBuilder::new();
-                let mut object_builder = variant_builder.new_object();
-
-                for i in start_offset..*end_offset {
-                    let value = values.value(i as usize);
-                    object_builder.insert(key_strings.value(i as usize), value);
-                }
-                object_builder.finish();
-                let (metadata, value) = variant_builder.finish();
-                let variant = Variant::try_new(&metadata, &value)?;
+    // Create row builder for the input array type
+    let mut row_builder = make_arrow_to_variant_row_builder(input.data_type(), input, options)?;
 
-                builder.append_variant(variant);
+    // Create output array builder
+    let mut array_builder = VariantArrayBuilder::new(input.len());
 
-                start_offset += length as i32;
-            }
-        }
-        _ => {
-            return Err(ArrowError::CastError(format!(
-                "Unsupported map field type for casting to Variant: {field:?}",
-            )));
-        }
+    // Process each row using the row builder
+    for i in 0..input.len() {

Review Comment:
   that is certainly nice



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,

Review Comment:
   this is quite clever



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {

Review Comment:
   ANother reason that a batch API might be better



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2336977810


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2500 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(builder, index),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+    options: &'a CastOptions,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => {
+            ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Date64 => {
+            ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array, options))
+        }
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::Time32Second(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::Time32Millisecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::Time64Microsecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::Time64Nanosecond(
+                TimeArrowToVariantBuilder::new(array, options),
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => {
+            ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Struct(_) => ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(
+            array.as_struct(),
+            options,
+        )?),
+        DataType::Map(_, _) => {
+            ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array, options)?)
+        }
+        DataType::Dictionary(_, _) => ArrowToVariantRowBuilder::Dictionary(
+            DictionaryArrowToVariantBuilder::new(array, options)?,
+        ),
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array, options)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+///
+/// The macro optionally allows to define a transform for values read from the underlying
+/// array. Transforms of the form `|value| { ... }` are infallible (and should produce something
+/// that implements `Into<Variant>`), while transforms of the form `|value| -> Option<_> { ... }`
+/// are fallible (and should produce `Option<impl Into<Variant>>`); a failed tarnsform will either
+/// append null to the builder or return an error, depending on cast options.
+///
+/// Also supports optional extra fields that are passed to the constructor and which are available
+/// by reference in the value transform. Providing a fallible value transform requires also
+/// providing the extra field `options: &'a CastOptions`.
+// TODO: If/when the macro_metavar_expr feature stabilizes, the `ignore` meta-function would allow
+// us to "use" captured tokens without emitting them:
+//
+// ```
+// $(
+//     ${ignore($value)}
+//     $(
+//         ${ignore($option_ty)}
+//         options: &$lifetime CastOptions,
+//     )?
+// )?
+// ```
+//
+// That, in turn, would allow us to inject the `options` field whenever the user specifies a
+// fallible value transform, instead of requiring them to manually define it. This might not be
+// worth the trouble, tho, because it makes for some pretty bulky and unwieldy macro expansions.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
+        $( where $where_path:path: $where_bound:path $(,)? )?
+        $({ $($field:ident: $field_type:ty),+ $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr }
+        $(, |$value:ident| $(-> Option<$option_ty:ty>)? $value_transform:expr)?
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $bound )?>
+        $( where $where_path: $where_bound )?
+        {
+            array: &$lifetime $array_type,
+            $( $( $field: $field_type, )+ )?
+        }
+
+        impl<$lifetime $(, $generic: $bound+ )?> $name<$lifetime $(, $generic)?>
+        $( where $where_path: $where_bound )?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $( $field: $field_type ),+ )?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $( $( $field, )+ )?
+                }
+            }
+
+            fn append_row(&self, builder: &mut impl VariantBuilderExt, index: usize) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    // Macro hygiene: Give any extra fields names the value transform can access.
+                    //
+                    // The value transform doesn't normally reference cast options, but the macro's
+                    // caller still has to declare the field because stable rust has no way to "use"
+                    // a captured token without emitting it. So, silence unused variable warnings,
+                    // assuming that's the `options` field. Unfortunately, that also silences
+                    // legitimate compiler warnings if an infallible value transform fails to use
+                    // its first extra field.
+                    $(
+                        #[allow(unused)]
+                        $( let $field = &self.$field; )+
+                    )?
+
+                    // Apply the value transform, if any (with name swapping for hygiene)
+                    let value = self.array.value(index);
+                    $(
+                        let $value = value;
+                        let value = $value_transform;
+                        $(
+                            // NOTE: The `?` macro expansion fails without the type annotation.
+                            let Some(value): Option<$option_ty> = value else {
+                                if self.options.strict {
+                                    return Err(ArrowError::ComputeError(format!(
+                                        "Failed to convert value at index {index}: conversion failed",
+                                    )));
+                                } else {
+                                    builder.append_null();
+                                    return Ok(());
+                                }
+                            };
+                        )?
+                    )?
+                    builder.append_value(value);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() }
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() }
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        options: &'a CastOptions,
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        // Convert using Arrow's temporal conversion functions
+        as_datetime::<T>(value).map(|naive_datetime| {
+            if *has_time_zone {
+                // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+                let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+                Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+            } else {
+                // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+                Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+            }
+        })
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() }
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() }
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() }
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() }
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() }
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        _index: usize,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(&mut list_builder, value_index)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(
+        struct_array: &'a arrow::array::StructArray,
+        options: &'a CastOptions,
+    ) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder = make_arrow_to_variant_row_builder(
+                field_array.data_type(),
+                field_array.as_ref(),
+                options,
+            )?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(&mut field_builder, index)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(&mut field_builder, kv_index)?;
+        }
+
+        object_builder.finish();
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+///
+/// NOTE: Union type ids are _not_ required to be dense, hence the hash map for child builders.
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder = make_arrow_to_variant_row_builder(
+                child_array.data_type(),
+                child_array.as_ref(),
+                options,
+            )?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(builder, value_offset)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(builder, normalized_key)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(builder, self.run_number)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::VariantArrayBuilder;
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    #[test]
+    fn test_primitive_row_builder() {
+        // Test Int32Array
+        let int_array = Int32Array::from(vec![Some(42), None, Some(100)]);
+        let options = CastOptions::default();
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(int_array.data_type(), &int_array, &options).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 0).unwrap();
+        variant_builder.finish();
+
+        // Test null value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 1).unwrap();
+        variant_builder.finish();
+
+        // Test second value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(&mut variant_builder, 2).unwrap();
+        variant_builder.finish();

Review Comment:
   Thanks for pointing out the boilerplate explosion. It should be fixed in the latest commit (~400 LoC eliminated)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3276167490

   Awesome -- thanks @scovich  and @codephage2020  -- this is really cool


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3275281550

   Once this merges, I did some pathfinding on the bulk append and will push a couple (stacked) PR to start the discussion.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "alamb (via GitHub)" <gi...@apache.org>.
alamb merged PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2331825012


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {

Review Comment:
   This was easily the most annoying part of the whole columnar/row-builder transformation -- there's technically no guarantee that subsequent `append_row` calls use sequentially increasing index values, so we have to handle jumps both forward and backward while hopefully being efficient in the common case of sequential index values.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3274646490

   @alamb -- I have a few cosmetic cleanups to push (in a few minutes) and then this can merge whenever you're ready


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3272348634

   Re
   
   > My biggest comment / suggestion is to consider making the API vectorized (convert the entire Arrow Array) but I think we can do that as a follow on PR
   
   And https://github.com/apache/arrow-rs/pull/8299#discussion_r2334767295 -- that run-end encoding could be handled more easily in a vectorized API.
   
   And https://github.com/apache/arrow-rs/pull/8299#discussion_r2334753988 that suggests an `append_all_rows()` method.
   
   And https://github.com/apache/arrow-rs/pull/8299#discussion_r2334742498 that also wonders about vectorization.
   
   I'll try to give one response that covers them all:
   
   I think it's reasonable to consider adding a bulk append type API, but we have to be cognizant of the limitations and challenges it will face:
   * We will need a new trait that knows how to create (and finish!) variant builder instances
   * Variant building is inherently row-based, so any builder that ultimately needs to produce a variant array or variant object as its output will have a trivial `append_all_rows` that just calls `append_row` in a loop (like today), in order to recursively build up the fields/elements of the variant it creates.
   * The API would be very nice for converting primitive arrays to variant, because they don't need to recurse on anything. Also nice because we could potentially define a specialized impl just for `VariantArrayBuilder`, so we don't have to deal with that new variant builder create+finish trait.
   * Casting a list of primitive values is an interesting intermediate case, where one _should_ be able to append all the elements of a given list in one shot. But that _might_ require the new create+finish trait? Or maybe it just needs a second specialization for `ListBuilder`?
   * Maybe instead of a no-arg `append_all_rows()`, we should consider a ranged `append_many_rows(start..end)`? One could always pass `..` to request encoding of all rows.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3268639520

   CC @alamb @carpecodeum 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "klion26 (via GitHub)" <gi...@apache.org>.
klion26 commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3273475967

   > Given the amount of code in this PR and likelihood of conflicts, I would like to merge it sooner rather than later. Please everyone give a shout out if they want more time to review, or if they are happy to review post merge
   
   I'm happy to review post-merge. I'm following/understanding the code now, and don't have any blocking issue for now. The implementation is cool; I've learned a lot from this PR. I will post my comments if there are any, after the PR has been merged.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2331769583


##########
parquet-variant-json/src/from_json.rs:
##########
@@ -132,25 +129,6 @@ fn append_json(json: &Value, builder: &mut impl VariantBuilderExt) -> Result<(),
     Ok(())
 }
 
-struct ObjectFieldBuilder<'o, 'v, 's> {

Review Comment:
   Moved to builders.rs
   
   TBH, I never quite understood why it was defined here in the first place.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########


Review Comment:
   This is ~800 LoC (much of that boilerplate), plus 1600 LoC of new unit tests (covering all supported types).



##########
parquet-variant-compute/src/cast_to_variant.rs:
##########
@@ -76,551 +50,25 @@ use parquet_variant::{
 /// will be truncated to
 /// `1970-01-01T00:00:01.234567Z`
 pub fn cast_to_variant(input: &dyn Array) -> Result<VariantArray, ArrowError> {
-    let mut builder = VariantArrayBuilder::new(input.len());
+    // Create row builder for the input array type
+    let mut row_builder = make_arrow_to_variant_row_builder(input.data_type(), input)?;
 
-    let input_type = input.data_type();
-    match input_type {
-        DataType::Null => {
-            for _ in 0..input.len() {
-                builder.append_null();
-            }
-        }
-        DataType::Boolean => {
-            non_generic_conversion_array!(input.as_boolean(), |v| v, builder);
-        }
-        DataType::Int8 => {
-            primitive_conversion_array!(Int8Type, input, builder);
-        }
-        DataType::Int16 => {
-            primitive_conversion_array!(Int16Type, input, builder);
-        }
-        DataType::Int32 => {
-            primitive_conversion_array!(Int32Type, input, builder);
-        }
-        DataType::Int64 => {
-            primitive_conversion_array!(Int64Type, input, builder);
-        }
-        DataType::UInt8 => {
-            primitive_conversion_array!(UInt8Type, input, builder);
-        }
-        DataType::UInt16 => {
-            primitive_conversion_array!(UInt16Type, input, builder);
-        }
-        DataType::UInt32 => {
-            primitive_conversion_array!(UInt32Type, input, builder);
-        }
-        DataType::UInt64 => {
-            primitive_conversion_array!(UInt64Type, input, builder);
-        }
-        DataType::Float16 => {
-            generic_conversion_array!(Float16Type, as_primitive, f32::from, input, builder);
-        }
-        DataType::Float32 => {
-            primitive_conversion_array!(Float32Type, input, builder);
-        }
-        DataType::Float64 => {
-            primitive_conversion_array!(Float64Type, input, builder);
-        }
-        DataType::Decimal32(_, scale) => {
-            generic_conversion_array!(
-                Decimal32Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i32, VariantDecimal4),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal64(_, scale) => {
-            generic_conversion_array!(
-                Decimal64Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i64, VariantDecimal8),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal128(_, scale) => {
-            generic_conversion_array!(
-                Decimal128Type,
-                as_primitive,
-                |v| decimal_to_variant_decimal!(v, scale, i128, VariantDecimal16),
-                input,
-                builder
-            );
-        }
-        DataType::Decimal256(_, scale) => {
-            generic_conversion_array!(
-                Decimal256Type,
-                as_primitive,
-                |v: i256| {
-                    // Since `i128::MAX` is larger than the max value of `VariantDecimal16`,
-                    // any `i256` value that cannot be cast to `i128` is unable to be cast to `VariantDecimal16` either.
-                    // Therefore, we can safely convert `i256` to `i128` first and process it like `i128`.
-                    if let Some(v) = v.to_i128() {
-                        decimal_to_variant_decimal!(v, scale, i128, VariantDecimal16)
-                    } else {
-                        Variant::Null
-                    }
-                },
-                input,
-                builder
-            );
-        }
-        DataType::Timestamp(time_unit, time_zone) => {
-            convert_timestamp(time_unit, time_zone, input, &mut builder);
-        }
-        DataType::Date32 => {
-            generic_conversion_array!(
-                Date32Type,
-                as_primitive,
-                |v: i32| -> NaiveDate { Date32Type::to_naive_date(v) },
-                input,
-                builder
-            );
-        }
-        DataType::Date64 => {
-            generic_conversion_array!(
-                Date64Type,
-                as_primitive,
-                |v: i64| { Date64Type::to_naive_date_opt(v).unwrap() },
-                input,
-                builder
-            );
-        }
-        DataType::Time32(unit) => {
-            match *unit {
-                TimeUnit::Second => {
-                    generic_conversion_array!(
-                        Time32SecondType,
-                        as_primitive,
-                        // nano second are always 0
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(v as u32, 0u32).unwrap(),
-                        input,
-                        builder
-                    );
-                }
-                TimeUnit::Millisecond => {
-                    generic_conversion_array!(
-                        Time32MillisecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            v as u32 / 1000,
-                            (v as u32 % 1000) * 1_000_000
-                        )
-                        .unwrap(),
-                        input,
-                        builder
-                    );
-                }
-                _ => {
-                    return Err(ArrowError::CastError(format!(
-                        "Unsupported Time32 unit: {:?}",
-                        unit
-                    )));
-                }
-            };
-        }
-        DataType::Time64(unit) => {
-            match *unit {
-                TimeUnit::Microsecond => {
-                    generic_conversion_array!(
-                        Time64MicrosecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            (v / 1_000_000) as u32,
-                            (v % 1_000_000 * 1_000) as u32
-                        )
-                        .unwrap(),
-                        input,
-                        builder
-                    );
-                }
-                TimeUnit::Nanosecond => {
-                    generic_conversion_array!(
-                        Time64NanosecondType,
-                        as_primitive,
-                        |v| NaiveTime::from_num_seconds_from_midnight_opt(
-                            (v / 1_000_000_000) as u32,
-                            (v % 1_000_000_000) as u32
-                        )
-                        .unwrap(),
-                        input,
-                        builder
-                    );
-                }
-                _ => {
-                    return Err(ArrowError::CastError(format!(
-                        "Unsupported Time64 unit: {:?}",
-                        unit
-                    )));
-                }
-            };
-        }
-        DataType::Duration(_) | DataType::Interval(_) => {
-            return Err(ArrowError::InvalidArgumentError(
-                "Casting duration/interval types to Variant is not supported. \
-                 The Variant format does not define duration/interval types."
-                    .to_string(),
-            ));
-        }
-        DataType::Binary => {
-            generic_conversion_array!(BinaryType, as_bytes, |v| v, input, builder);
-        }
-        DataType::LargeBinary => {
-            generic_conversion_array!(LargeBinaryType, as_bytes, |v| v, input, builder);
-        }
-        DataType::BinaryView => {
-            generic_conversion_array!(BinaryViewType, as_byte_view, |v| v, input, builder);
-        }
-        DataType::FixedSizeBinary(_) => {
-            non_generic_conversion_array!(input.as_fixed_size_binary(), |v| v, builder);
-        }
-        DataType::Utf8 => {
-            generic_conversion_array!(i32, as_string, |v| v, input, builder);
-        }
-        DataType::LargeUtf8 => {
-            generic_conversion_array!(i64, as_string, |v| v, input, builder);
-        }
-        DataType::Utf8View => {
-            non_generic_conversion_array!(input.as_string_view(), |v| v, builder);
-        }
-        DataType::List(_) => convert_list::<i32>(input, &mut builder)?,
-        DataType::LargeList(_) => convert_list::<i64>(input, &mut builder)?,
-        DataType::Struct(_) => convert_struct(input, &mut builder)?,
-        DataType::Map(field, _) => convert_map(field, input, &mut builder)?,
-        DataType::Union(fields, _) => convert_union(fields, input, &mut builder)?,
-        DataType::Dictionary(_, _) => convert_dictionary_encoded(input, &mut builder)?,
-        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
-            DataType::Int16 => convert_run_end_encoded::<Int16Type>(input, &mut builder)?,
-            DataType::Int32 => convert_run_end_encoded::<Int32Type>(input, &mut builder)?,
-            DataType::Int64 => convert_run_end_encoded::<Int64Type>(input, &mut builder)?,
-            _ => {
-                return Err(ArrowError::CastError(format!(
-                    "Unsupported run ends type: {:?}",
-                    run_ends.data_type()
-                )));
-            }
-        },
-        dt => {
-            return Err(ArrowError::CastError(format!(
-                "Unsupported data type for casting to Variant: {dt:?}",
-            )));
-        }
-    };
-    Ok(builder.build())
-}
+    // Create output array builder
+    let mut array_builder = VariantArrayBuilder::new(input.len());
 
-// TODO do we need a cast_with_options to allow specifying conversion behavior,
-// e.g. how to handle overflows, whether to convert to Variant::Null or return
-// an error, etc. ?
-
-/// Convert timestamp arrays to native datetimes
-fn convert_timestamp(
-    time_unit: &TimeUnit,
-    time_zone: &Option<Arc<str>>,
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) {
-    let native_datetimes: Vec<Option<NaiveDateTime>> = match time_unit {
-        arrow_schema::TimeUnit::Second => {
-            let ts_array = input
-                .as_any()
-                .downcast_ref::<TimestampSecondArray>()
-                .expect("Array is not TimestampSecondArray");
-
-            ts_array
-                .iter()
-                .map(|x| x.map(|y| timestamp_s_to_datetime(y).unwrap()))
-                .collect()
-        }
-        arrow_schema::TimeUnit::Millisecond => {
-            let ts_array = input
-                .as_any()
-                .downcast_ref::<TimestampMillisecondArray>()
-                .expect("Array is not TimestampMillisecondArray");
-
-            ts_array
-                .iter()
-                .map(|x| x.map(|y| timestamp_ms_to_datetime(y).unwrap()))
-                .collect()
-        }
-        arrow_schema::TimeUnit::Microsecond => {
-            let ts_array = input
-                .as_any()
-                .downcast_ref::<TimestampMicrosecondArray>()
-                .expect("Array is not TimestampMicrosecondArray");
-            ts_array
-                .iter()
-                .map(|x| x.map(|y| timestamp_us_to_datetime(y).unwrap()))
-                .collect()
-        }
-        arrow_schema::TimeUnit::Nanosecond => {
-            let ts_array = input
-                .as_any()
-                .downcast_ref::<TimestampNanosecondArray>()
-                .expect("Array is not TimestampNanosecondArray");
-            ts_array
-                .iter()
-                .map(|x| x.map(|y| timestamp_ns_to_datetime(y).unwrap()))
-                .collect()
-        }
-    };
-
-    for x in native_datetimes {
-        match x {
-            Some(ndt) => {
-                if time_zone.is_none() {
-                    builder.append_variant(ndt.into());
-                } else {
-                    let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&ndt);
-                    builder.append_variant(utc_dt.into());
-                }
-            }
-            None => {
-                builder.append_null();
-            }
-        }
-    }
-}
-
-/// Generic function to convert list arrays (both List and LargeList) to variant arrays
-fn convert_list<O: OffsetSizeTrait>(
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    let list_array = input.as_list::<O>();
-    let values = list_array.values();
-    let offsets = list_array.offsets();
-
-    let first_offset = *offsets.first().expect("There should be an offset");
-    let length = *offsets.last().expect("There should be an offset") - first_offset;
-    let sliced_values = values.slice(first_offset.as_usize(), length.as_usize());
-
-    let values_variant_array = cast_to_variant(sliced_values.as_ref())?;
-    let new_offsets = OffsetBuffer::new(ScalarBuffer::from_iter(
-        offsets.iter().map(|o| *o - first_offset),
-    ));
-
-    for i in 0..list_array.len() {
-        if list_array.is_null(i) {
-            builder.append_null();
-            continue;
-        }
-
-        let start = new_offsets[i].as_usize();
-        let end = new_offsets[i + 1].as_usize();
-
-        // Start building the inner VariantList
-        let mut variant_builder = VariantBuilder::new();
-        let mut list_builder = variant_builder.new_list();
-
-        // Add all values from the slice
-        for j in start..end {
-            list_builder.append_value(values_variant_array.value(j));
-        }
-
-        list_builder.finish();
-
-        let (metadata, value) = variant_builder.finish();
-        let variant = Variant::new(&metadata, &value);
-        builder.append_variant(variant)
+    // Process each row using the row builder
+    for i in 0..input.len() {
+        let mut builder = array_builder.variant_builder();
+        row_builder.append_row(i, &mut builder)?;
+        builder.finish();
     }
 
-    Ok(())
+    Ok(array_builder.build())
 }
 
-fn convert_struct(input: &dyn Array, builder: &mut VariantArrayBuilder) -> Result<(), ArrowError> {
-    let struct_array = input.as_struct();
-
-    // Pre-convert all field arrays once for better performance
-    // This avoids converting the same field array multiple times
-    // Alternative approach: Use slicing per row: field_array.slice(i, 1)
-    // However, pre-conversion is more efficient for typical use cases
-    let field_variant_arrays: Result<Vec<_>, _> = struct_array
-        .columns()
-        .iter()
-        .map(|field_array| cast_to_variant(field_array.as_ref()))
-        .collect();
-    let field_variant_arrays = field_variant_arrays?;
-
-    // Cache column names to avoid repeated calls
-    let column_names = struct_array.column_names();
-
-    for i in 0..struct_array.len() {
-        if struct_array.is_null(i) {
-            builder.append_null();
-            continue;
-        }
-
-        // Create a VariantBuilder for this struct instance
-        let mut variant_builder = VariantBuilder::new();
-        let mut object_builder = variant_builder.new_object();
-
-        // Iterate through all fields in the struct
-        for (field_idx, field_name) in column_names.iter().enumerate() {
-            // Use pre-converted field variant arrays for better performance
-            // Check nulls directly from the pre-converted arrays instead of accessing column again
-            if !field_variant_arrays[field_idx].is_null(i) {
-                let field_variant = field_variant_arrays[field_idx].value(i);
-                object_builder.insert(field_name, field_variant);
-            }
-            // Note: we skip null fields rather than inserting Variant::Null
-            // to match Arrow struct semantics where null fields are omitted
-        }
-
-        object_builder.finish();
-        let (metadata, value) = variant_builder.finish();
-        let variant = Variant::try_new(&metadata, &value)?;
-        builder.append_variant(variant);
-    }
-
-    Ok(())
-}
-
-fn convert_map(
-    field: &FieldRef,
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    match field.data_type() {
-        DataType::Struct(_) => {
-            let map_array = input.as_map();
-            let keys = cast(map_array.keys(), &DataType::Utf8)?;
-            let key_strings = keys.as_string::<i32>();
-            let values = cast_to_variant(map_array.values())?;
-            let offsets = map_array.offsets();
-
-            let mut start_offset = offsets[0];
-            for end_offset in offsets.iter().skip(1) {
-                if start_offset >= *end_offset {
-                    builder.append_null();
-                    continue;
-                }
-
-                let length = (end_offset - start_offset) as usize;
-
-                let mut variant_builder = VariantBuilder::new();
-                let mut object_builder = variant_builder.new_object();
-
-                for i in start_offset..*end_offset {
-                    let value = values.value(i as usize);
-                    object_builder.insert(key_strings.value(i as usize), value);
-                }
-                object_builder.finish();
-                let (metadata, value) = variant_builder.finish();
-                let variant = Variant::try_new(&metadata, &value)?;
-
-                builder.append_variant(variant);
-
-                start_offset += length as i32;
-            }
-        }
-        _ => {
-            return Err(ArrowError::CastError(format!(
-                "Unsupported map field type for casting to Variant: {field:?}",
-            )));
-        }
-    }
-
-    Ok(())
-}
-
-fn convert_union(
-    fields: &UnionFields,
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    let union_array = input.as_union();
-
-    // Convert each child array to variant arrays
-    let mut child_variant_arrays = HashMap::new();
-    for (type_id, _) in fields.iter() {
-        let child_array = union_array.child(type_id);
-        let child_variant_array = cast_to_variant(child_array.as_ref())?;
-        child_variant_arrays.insert(type_id, child_variant_array);
-    }
-
-    // Process each element in the union array
-    for i in 0..union_array.len() {
-        let type_id = union_array.type_id(i);
-        let value_offset = union_array.value_offset(i);
-
-        if let Some(child_variant_array) = child_variant_arrays.get(&type_id) {
-            if child_variant_array.is_null(value_offset) {
-                builder.append_null();
-            } else {
-                let value = child_variant_array.value(value_offset);
-                builder.append_variant(value);
-            }
-        } else {
-            // This should not happen in a valid union, but handle gracefully
-            builder.append_null();
-        }
-    }
-
-    Ok(())
-}
-
-fn convert_dictionary_encoded(
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    let dict_array = input.as_any_dictionary();
-    let values_variant_array = cast_to_variant(dict_array.values().as_ref())?;
-    let normalized_keys = dict_array.normalized_keys();
-    let keys = dict_array.keys();
-
-    for (i, key_idx) in normalized_keys.iter().enumerate() {
-        if keys.is_null(i) {
-            builder.append_null();
-            continue;
-        }
-
-        if values_variant_array.is_null(*key_idx) {
-            builder.append_null();
-            continue;
-        }
-
-        let value = values_variant_array.value(*key_idx);
-        builder.append_variant(value);
-    }
-
-    Ok(())
-}
-
-fn convert_run_end_encoded<R: RunEndIndexType>(
-    input: &dyn Array,
-    builder: &mut VariantArrayBuilder,
-) -> Result<(), ArrowError> {
-    let run_array = input.as_run::<R>();
-    let values_variant_array = cast_to_variant(run_array.values().as_ref())?;
-
-    // Process runs in batches for better performance
-    let run_ends = run_array.run_ends().values();
-    let mut logical_start = 0;
-
-    for (physical_idx, &run_end) in run_ends.iter().enumerate() {
-        let logical_end = run_end.as_usize();
-        let run_length = logical_end - logical_start;
-
-        if values_variant_array.is_null(physical_idx) {
-            // Append nulls for the entire run
-            for _ in 0..run_length {
-                builder.append_null();
-            }
-        } else {
-            // Get the value once and append it for the entire run
-            let value = values_variant_array.value(physical_idx);
-            for _ in 0..run_length {
-                builder.append_variant(value.clone());
-            }
-        }
-
-        logical_start = logical_end;
-    }
-
-    Ok(())
-}
+// TODO do we need a cast_with_options to allow specifying conversion behavior,
+// e.g. how to handle overflows, whether to convert to Variant::Null or return
+// an error, etc. ?

Review Comment:
   Neither changed nor moved... it's just diff weirdness.



##########
parquet-variant-compute/src/variant_array_builder.rs:
##########
@@ -199,9 +199,14 @@ pub struct VariantArrayVariantBuilder<'a> {
     metadata_offsets: &'a mut Vec<usize>,
     value_offsets: &'a mut Vec<usize>,
     nulls: &'a mut NullBufferBuilder,
+    is_null: bool,
 }
 
 impl VariantBuilderExt for VariantArrayVariantBuilder<'_> {
+    /// Appending NULL to a variant array produces an actual NULL value
+    fn append_null(&mut self) {

Review Comment:
   This new `append_null` capability was critical to making all this work:
   * Appending NULL to a variant array builder produces an actual NULL
   * Appending NULL to an object field builder is a no-op (field is missing, as per variant spec)
   * Otherwise, appending NULL to a variant value or variant list produces `Variant::Null`



##########
parquet-variant-compute/src/cast_to_variant.rs:
##########


Review Comment:
   This file was mostly gutted (but unit tests left in place), with the real work now being done by row builders defined in arrow_to_variant.rs via `make_arrow_to_variant_row_builder`.



##########
parquet-variant-compute/src/cast_to_variant.rs:
##########
@@ -2099,33 +1554,64 @@ mod tests {
     }
 
     #[test]
-    fn test_cast_to_variant_map_with_nulls() {
-        let keys = vec!["key1", "key2", "key3"];
-        let values_data = Int32Array::from(vec![1, 2, 3]);
-        let entry_offsets = vec![0, 1, 1, 3];
-        let map_array =
-            MapArray::new_from_strings(keys.clone().into_iter(), &values_data, &entry_offsets)
-                .unwrap();
+    fn test_cast_to_variant_map_with_nulls_and_empty() {

Review Comment:
   * See https://github.com/apache/arrow-rs/issues/8289
   
   I had to fix this test because otherwise the correct behavior of the new map row builder would fail it.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,

Review Comment:
   A borrowed reference to this extra param is made available to the `|value|` "lambda"



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {

Review Comment:
   Using an enum because enum variant selection is consistently faster than virtual function dispatch. 
   Plus, it allows us to use the non-dyn-compat `VariantBuilderExt`. 
   As a bonus, it allows the compiler to specialize these implementations for all four kinds of builders they will encounter. 



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;

Review Comment:
   This is one of the reasons it doesn't work to just visit the actual arrays... sometimes columnar pre-work is needed. Dictionary and run-end encoding are two more examples.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };

Review Comment:
   NOTE: I'm pretty sure the original code did not handle this corner case



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::VariantArrayBuilder;
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    #[test]
+    fn test_primitive_row_builder() {
+        // Test Int32Array
+        let int_array = Int32Array::from(vec![Some(42), None, Some(100)]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(int_array.data_type(), &int_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test null value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test second value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::Int32(42));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::Int32(100));
+    }
+
+    #[test]
+    fn test_string_row_builder() {
+        let string_array = StringArray::from(vec![Some("hello"), None, Some("world")]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(string_array.data_type(), &string_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::from("hello"));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::from("world"));
+    }
+
+    #[test]
+    fn test_boolean_row_builder() {
+        let bool_array = BooleanArray::from(vec![Some(true), None, Some(false)]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(bool_array.data_type(), &bool_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::from(true));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::from(false));
+    }
+
+    #[test]
+    fn test_struct_row_builder() {
+        use arrow::array::{ArrayRef, Int32Array, StringArray, StructArray};
+        use arrow_schema::{DataType, Field};
+        use std::sync::Arc;
+
+        // Create a struct array with int and string fields
+        let int_field = Field::new("id", DataType::Int32, true);
+        let string_field = Field::new("name", DataType::Utf8, true);
+
+        let int_array = Int32Array::from(vec![Some(1), None, Some(3)]);
+        let string_array = StringArray::from(vec![Some("Alice"), Some("Bob"), None]);
+
+        let struct_array = StructArray::try_new(
+            vec![int_field, string_field].into(),
+            vec![
+                Arc::new(int_array) as ArrayRef,
+                Arc::new(string_array) as ArrayRef,
+            ],
+            None,
+        )
+        .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(struct_array.data_type(), &struct_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first row
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test second row (with null int field)
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test third row (with null string field)
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+
+        // Check first row - should have both fields
+        let first_variant = variant_array.value(0);
+        assert_eq!(first_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            first_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+
+        // Check second row - should have name field but not id (null field omitted)
+        let second_variant = variant_array.value(1);
+        assert_eq!(second_variant.get_object_field("id"), None); // null field omitted
+        assert_eq!(
+            second_variant.get_object_field("name"),
+            Some(Variant::from("Bob"))
+        );
+
+        // Check third row - should have id field but not name (null field omitted)
+        let third_variant = variant_array.value(2);
+        assert_eq!(third_variant.get_object_field("id"), Some(Variant::from(3)));
+        assert_eq!(third_variant.get_object_field("name"), None); // null field omitted
+    }
+
+    #[test]
+    fn test_run_end_encoded_row_builder() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array: [A, A, B, B, B, C]
+        // run_ends: [2, 5, 6]
+        // values: ["A", "B", "C"]
+        let values = StringArray::from(vec!["A", "B", "C"]);
+        let run_ends = Int32Array::from(vec![2, 5, 6]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(6);
+
+        // Test sequential access (most common case)
+        for i in 0..6 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 6);
+
+        // Verify the values
+        assert_eq!(variant_array.value(0), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(1), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(2), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(3), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(4), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(5), Variant::from("C")); // Run 2
+    }
+
+    #[test]
+    fn test_run_end_encoded_random_access() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array: [A, A, B, B, B, C]
+        let values = StringArray::from(vec!["A", "B", "C"]);
+        let run_ends = Int32Array::from(vec![2, 5, 6]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+
+        // Test random access pattern (backward jumps, forward jumps)
+        let access_pattern = [0, 5, 2, 4, 1, 3]; // Mix of all cases
+        let expected_values = ["A", "C", "B", "B", "A", "B"];
+
+        for (i, &index) in access_pattern.iter().enumerate() {
+            let mut array_builder = VariantArrayBuilder::new(1);
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(index, &mut variant_builder).unwrap();
+            variant_builder.finish();
+
+            let variant_array = array_builder.build();
+            assert_eq!(variant_array.value(0), Variant::from(expected_values[i]));
+        }
+    }
+
+    #[test]
+    fn test_run_end_encoded_with_nulls() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array with null values: [A, A, null, null, B]
+        let values = StringArray::from(vec![Some("A"), None, Some("B")]);
+        let run_ends = Int32Array::from(vec![2, 4, 5]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values
+        assert_eq!(variant_array.value(0), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(1), Variant::from("A")); // Run 0
+        assert!(variant_array.is_null(2)); // Run 1 (null)
+        assert!(variant_array.is_null(3)); // Run 1 (null)
+        assert_eq!(variant_array.value(4), Variant::from("B")); // Run 2
+    }
+
+    #[test]
+    fn test_dictionary_row_builder() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array: keys=[0, 1, 0, 2, 1], values=["apple", "banana", "cherry"]
+        let values = StringArray::from(vec!["apple", "banana", "cherry"]);
+        let keys = Int32Array::from(vec![0, 1, 0, 2, 1]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values match the dictionary lookup
+        assert_eq!(variant_array.value(0), Variant::from("apple")); // keys[0] = 0 -> values[0] = "apple"
+        assert_eq!(variant_array.value(1), Variant::from("banana")); // keys[1] = 1 -> values[1] = "banana"
+        assert_eq!(variant_array.value(2), Variant::from("apple")); // keys[2] = 0 -> values[0] = "apple"
+        assert_eq!(variant_array.value(3), Variant::from("cherry")); // keys[3] = 2 -> values[2] = "cherry"
+        assert_eq!(variant_array.value(4), Variant::from("banana")); // keys[4] = 1 -> values[1] = "banana"
+    }
+
+    #[test]
+    fn test_dictionary_with_nulls() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array with null keys: keys=[0, null, 1, null, 2], values=["x", "y", "z"]
+        let values = StringArray::from(vec!["x", "y", "z"]);
+        let keys = Int32Array::from(vec![Some(0), None, Some(1), None, Some(2)]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values and nulls
+        assert_eq!(variant_array.value(0), Variant::from("x")); // keys[0] = 0 -> values[0] = "x"
+        assert!(variant_array.is_null(1)); // keys[1] = null
+        assert_eq!(variant_array.value(2), Variant::from("y")); // keys[2] = 1 -> values[1] = "y"
+        assert!(variant_array.is_null(3)); // keys[3] = null
+        assert_eq!(variant_array.value(4), Variant::from("z")); // keys[4] = 2 -> values[2] = "z"
+    }
+
+    #[test]
+    fn test_dictionary_random_access() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array: keys=[0, 1, 2, 0, 1, 2], values=["red", "green", "blue"]
+        let values = StringArray::from(vec!["red", "green", "blue"]);
+        let keys = Int32Array::from(vec![0, 1, 2, 0, 1, 2]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+
+        // Test random access pattern
+        let access_pattern = [5, 0, 3, 1, 4, 2]; // Random order
+        let expected_values = ["blue", "red", "red", "green", "green", "blue"];
+
+        for (i, &index) in access_pattern.iter().enumerate() {
+            let mut array_builder = VariantArrayBuilder::new(1);
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(index, &mut variant_builder).unwrap();
+            variant_builder.finish();
+
+            let variant_array = array_builder.build();
+            assert_eq!(variant_array.value(0), Variant::from(expected_values[i]));
+        }
+    }
+
+    #[test]
+    fn test_nested_dictionary() {
+        use arrow::array::{DictionaryArray, Int32Array, StructArray};
+        use arrow::datatypes::{Field, Int32Type};
+
+        // Create a dictionary with struct values
+        let id_array = Int32Array::from(vec![1, 2, 3]);
+        let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
+        let struct_array = StructArray::from(vec![
+            (
+                Arc::new(Field::new("id", DataType::Int32, false)),
+                Arc::new(id_array) as ArrayRef,
+            ),
+            (
+                Arc::new(Field::new("name", DataType::Utf8, false)),
+                Arc::new(name_array) as ArrayRef,
+            ),
+        ]);
+
+        let keys = Int32Array::from(vec![0, 1, 0, 2, 1]);
+        let dict_array =
+            DictionaryArray::<Int32Type>::try_new(keys, Arc::new(struct_array)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the nested struct values
+        let first_variant = variant_array.value(0);
+        assert_eq!(first_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            first_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+
+        let second_variant = variant_array.value(1);
+        assert_eq!(
+            second_variant.get_object_field("id"),
+            Some(Variant::from(2))
+        );
+        assert_eq!(
+            second_variant.get_object_field("name"),
+            Some(Variant::from("Bob"))
+        );
+
+        // Test that repeated keys give same values
+        let third_variant = variant_array.value(2);
+        assert_eq!(third_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            third_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+    }
+
+    #[test]
+    fn test_list_row_builder() {
+        use arrow::array::ListArray;
+
+        // Create a list array: [[1, 2], [3, 4, 5], null, []]
+        let data = vec![
+            Some(vec![Some(1), Some(2)]),
+            Some(vec![Some(3), Some(4), Some(5)]),
+            None,
+            Some(vec![]),
+        ];
+        let list_array = ListArray::from_iter_primitive::<Int32Type, _, _>(data);
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(list_array.data_type(), &list_array).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(list_array.len());
+
+        for i in 0..list_array.len() {
+            let mut builder = variant_array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 4);
+
+        // Row 0: [1, 2]
+        let row0 = variant_array.value(0);
+        let list0 = row0.as_list().unwrap();
+        assert_eq!(list0.len(), 2);
+        assert_eq!(list0.get(0), Some(Variant::from(1)));
+        assert_eq!(list0.get(1), Some(Variant::from(2)));
+
+        // Row 1: [3, 4, 5]
+        let row1 = variant_array.value(1);
+        let list1 = row1.as_list().unwrap();
+        assert_eq!(list1.len(), 3);
+        assert_eq!(list1.get(0), Some(Variant::from(3)));
+        assert_eq!(list1.get(1), Some(Variant::from(4)));
+        assert_eq!(list1.get(2), Some(Variant::from(5)));
+
+        // Row 2: null
+        assert!(variant_array.is_null(2));
+
+        // Row 3: []
+        let row3 = variant_array.value(3);
+        let list3 = row3.as_list().unwrap();
+        assert_eq!(list3.len(), 0);
+    }
+
+    #[test]
+    fn test_large_list_row_builder() {
+        use arrow::array::LargeListArray;
+
+        // Create a large list array: [[1, 2], null]
+        let data = vec![Some(vec![Some(1i64), Some(2i64)]), None];
+        let list_array = LargeListArray::from_iter_primitive::<Int64Type, _, _>(data);
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(list_array.data_type(), &list_array).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(list_array.len());
+
+        for i in 0..list_array.len() {
+            let mut builder = variant_array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 2);
+
+        // Row 0: [1, 2]
+        let row0 = variant_array.value(0);
+        let list0 = row0.as_list().unwrap();
+        assert_eq!(list0.len(), 2);
+        assert_eq!(list0.get(0), Some(Variant::from(1i64)));
+        assert_eq!(list0.get(1), Some(Variant::from(2i64)));
+
+        // Row 1: null
+        assert!(variant_array.is_null(1));
+    }
+
+    #[test]
+    fn test_sliced_list_row_builder() {
+        use arrow::array::ListArray;
+
+        // Create a list array: [[1, 2], [3, 4, 5], [6]]
+        let data = vec![
+            Some(vec![Some(1), Some(2)]),
+            Some(vec![Some(3), Some(4), Some(5)]),
+            Some(vec![Some(6)]),
+        ];
+        let list_array = ListArray::from_iter_primitive::<Int32Type, _, _>(data);
+
+        // Slice to get just the middle element: [[3, 4, 5]]
+        let sliced_array = list_array.slice(1, 1);
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(sliced_array.data_type(), &sliced_array).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(sliced_array.len());
+
+        // Test the single row
+        let mut builder = variant_array_builder.variant_builder();
+        row_builder.append_row(0, &mut builder).unwrap();
+        builder.finish();
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify result
+        assert_eq!(variant_array.len(), 1);
+
+        // Row 0: [3, 4, 5]
+        let row0 = variant_array.value(0);
+        let list0 = row0.as_list().unwrap();
+        assert_eq!(list0.len(), 3);
+        assert_eq!(list0.get(0), Some(Variant::from(3)));
+        assert_eq!(list0.get(1), Some(Variant::from(4)));
+        assert_eq!(list0.get(2), Some(Variant::from(5)));
+    }
+
+    #[test]
+    fn test_nested_list_row_builder() {
+        use arrow::array::ListArray;
+        use arrow::datatypes::Field;
+
+        // Build the nested structure manually
+        let inner_field = Arc::new(Field::new("item", DataType::Int32, true));
+        let inner_list_field = Arc::new(Field::new("item", DataType::List(inner_field), true));
+
+        let values_data = vec![Some(vec![Some(1), Some(2)]), Some(vec![Some(3)])];
+        let values_list = ListArray::from_iter_primitive::<Int32Type, _, _>(values_data);
+
+        let outer_offsets = arrow::buffer::OffsetBuffer::new(vec![0i32, 2, 2].into());
+        let outer_list = ListArray::new(
+            inner_list_field,
+            outer_offsets,
+            Arc::new(values_list),
+            Some(arrow::buffer::NullBuffer::from(vec![true, false])),
+        );
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(outer_list.data_type(), &outer_list).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(outer_list.len());
+
+        for i in 0..outer_list.len() {
+            let mut builder = variant_array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 2);
+
+        // Row 0: [[1, 2], [3]]
+        let row0 = variant_array.value(0);
+        let outer_list0 = row0.as_list().unwrap();
+        assert_eq!(outer_list0.len(), 2);
+
+        let inner_list0_0 = outer_list0.get(0).unwrap();
+        let inner_list0_0 = inner_list0_0.as_list().unwrap();
+        assert_eq!(inner_list0_0.len(), 2);
+        assert_eq!(inner_list0_0.get(0), Some(Variant::from(1)));
+        assert_eq!(inner_list0_0.get(1), Some(Variant::from(2)));
+
+        let inner_list0_1 = outer_list0.get(1).unwrap();
+        let inner_list0_1 = inner_list0_1.as_list().unwrap();
+        assert_eq!(inner_list0_1.len(), 1);
+        assert_eq!(inner_list0_1.get(0), Some(Variant::from(3)));
+
+        // Row 1: null
+        assert!(variant_array.is_null(1));
+    }
+
+    #[test]
+    fn test_map_row_builder() {
+        use arrow::array::{Int32Array, MapArray, StringArray, StructArray};
+        use arrow::buffer::{NullBuffer, OffsetBuffer};
+        use arrow::datatypes::{DataType, Field, Fields};
+        use std::sync::Arc;
+
+        // Create the entries struct array (key-value pairs)
+        let keys = StringArray::from(vec!["key1", "key2", "key3"]);
+        let values = Int32Array::from(vec![1, 2, 3]);
+        let entries_fields = Fields::from(vec![
+            Field::new("key", DataType::Utf8, false),
+            Field::new("value", DataType::Int32, true),
+        ]);
+        let entries = StructArray::new(
+            entries_fields.clone(),
+            vec![Arc::new(keys), Arc::new(values)],
+            None, // No nulls in the entries themselves
+        );
+
+        // Create offsets for 4 maps: [0..1], [1..1], [1..1], [1..3]
+        // Map 0: {"key1": 1}    (1 entry)
+        // Map 1: {}             (0 entries - empty)
+        // Map 2: null           (0 entries but NULL via null buffer)
+        // Map 3: {"key2": 2, "key3": 3}  (2 entries)
+        let offsets = OffsetBuffer::new(vec![0, 1, 1, 1, 3].into());
+
+        // Create null buffer - map at index 2 is NULL
+        let null_buffer = Some(NullBuffer::from(vec![true, true, false, true]));
+
+        // Create the map field
+        let map_field = Arc::new(Field::new(
+            "entries",
+            DataType::Struct(entries_fields),
+            false, // Keys are non-nullable
+        ));
+
+        // Create MapArray using try_new
+        let map_array = MapArray::try_new(
+            map_field,
+            offsets,
+            entries,
+            null_buffer,
+            false, // not ordered
+        )
+        .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(map_array.data_type(), &map_array).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(4);
+
+        // Test each row
+        for i in 0..4 {
+            let mut builder = variant_array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 4);
+
+        // Map 0: {"key1": 1}
+        let map0 = variant_array.value(0);
+        let obj0 = map0.as_object().unwrap();
+        assert_eq!(obj0.len(), 1);
+        assert_eq!(obj0.get("key1"), Some(Variant::from(1)));
+
+        // Map 1: {} (empty object, not null)
+        let map1 = variant_array.value(1);
+        let obj1 = map1.as_object().unwrap();
+        assert_eq!(obj1.len(), 0); // Empty object
+
+        // Map 2: null (actual NULL)
+        assert!(variant_array.is_null(2));
+
+        // Map 3: {"key2": 2, "key3": 3}
+        let map3 = variant_array.value(3);
+        let obj3 = map3.as_object().unwrap();
+        assert_eq!(obj3.len(), 2);
+        assert_eq!(obj3.get("key2"), Some(Variant::from(2)));
+        assert_eq!(obj3.get("key3"), Some(Variant::from(3)));
+    }
+
+    #[test]
+    fn test_union_sparse_row_builder() {
+        use arrow::array::{Float64Array, Int32Array, StringArray, UnionArray};
+        use arrow::buffer::ScalarBuffer;
+        use arrow::datatypes::{DataType, Field, UnionFields};
+        use std::sync::Arc;
+
+        // Create a sparse union array with mixed types (int, float, string)
+        let int_array = Int32Array::from(vec![Some(1), None, None, None, Some(34), None]);
+        let float_array = Float64Array::from(vec![None, Some(3.2), None, Some(32.5), None, None]);
+        let string_array = StringArray::from(vec![None, None, Some("hello"), None, None, None]);
+        let type_ids = [0, 1, 2, 1, 0, 0].into_iter().collect::<ScalarBuffer<i8>>();
+
+        let union_fields = UnionFields::new(
+            vec![0, 1, 2],
+            vec![
+                Field::new("int_field", DataType::Int32, false),
+                Field::new("float_field", DataType::Float64, false),
+                Field::new("string_field", DataType::Utf8, false),
+            ],
+        );
+
+        let children: Vec<Arc<dyn Array>> = vec![
+            Arc::new(int_array),
+            Arc::new(float_array),
+            Arc::new(string_array),
+        ];
+
+        let union_array = UnionArray::try_new(
+            union_fields,
+            type_ids,
+            None, // Sparse union
+            children,
+        )
+        .unwrap();
+
+        // Test the row builder
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(union_array.data_type(), &union_array).unwrap();
+
+        let mut variant_builder = VariantArrayBuilder::new(union_array.len());
+        for i in 0..union_array.len() {
+            let mut builder = variant_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+        let variant_array = variant_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 6);
+
+        // Row 0: int 1
+        assert_eq!(variant_array.value(0), Variant::Int32(1));
+
+        // Row 1: float 3.2
+        assert_eq!(variant_array.value(1), Variant::Double(3.2));
+
+        // Row 2: string "hello"
+        assert_eq!(variant_array.value(2), Variant::from("hello"));
+
+        // Row 3: float 32.5
+        assert_eq!(variant_array.value(3), Variant::Double(32.5));
+
+        // Row 4: int 34
+        assert_eq!(variant_array.value(4), Variant::Int32(34));
+
+        // Row 5: null (int array has null at this position)
+        assert!(variant_array.is_null(5));
+    }
+
+    #[test]
+    fn test_union_dense_row_builder() {
+        use arrow::array::{Float64Array, Int32Array, StringArray, UnionArray};
+        use arrow::buffer::ScalarBuffer;
+        use arrow::datatypes::{DataType, Field, UnionFields};
+        use std::sync::Arc;
+
+        // Create a dense union array with mixed types (int, float, string)
+        let int_array = Int32Array::from(vec![Some(1), Some(34), None]);
+        let float_array = Float64Array::from(vec![3.2, 32.5]);
+        let string_array = StringArray::from(vec!["hello"]);
+        let type_ids = [0, 1, 2, 1, 0, 0].into_iter().collect::<ScalarBuffer<i8>>();
+        let offsets = [0, 0, 0, 1, 1, 2]
+            .into_iter()
+            .collect::<ScalarBuffer<i32>>();
+
+        let union_fields = UnionFields::new(
+            vec![0, 1, 2],
+            vec![
+                Field::new("int_field", DataType::Int32, false),
+                Field::new("float_field", DataType::Float64, false),
+                Field::new("string_field", DataType::Utf8, false),
+            ],
+        );
+
+        let children: Vec<Arc<dyn Array>> = vec![
+            Arc::new(int_array),
+            Arc::new(float_array),
+            Arc::new(string_array),
+        ];
+
+        let union_array = UnionArray::try_new(
+            union_fields,
+            type_ids,
+            Some(offsets), // Dense union
+            children,
+        )
+        .unwrap();
+
+        // Test the row builder
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(union_array.data_type(), &union_array).unwrap();
+
+        let mut variant_builder = VariantArrayBuilder::new(union_array.len());
+        for i in 0..union_array.len() {
+            let mut builder = variant_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+        let variant_array = variant_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 6);
+
+        // Row 0: int 1 (offset 0 in int_array)
+        assert_eq!(variant_array.value(0), Variant::Int32(1));
+
+        // Row 1: float 3.2 (offset 0 in float_array)
+        assert_eq!(variant_array.value(1), Variant::Double(3.2));
+
+        // Row 2: string "hello" (offset 0 in string_array)
+        assert_eq!(variant_array.value(2), Variant::from("hello"));
+
+        // Row 3: float 32.5 (offset 1 in float_array)
+        assert_eq!(variant_array.value(3), Variant::Double(32.5));
+
+        // Row 4: int 34 (offset 1 in int_array)
+        assert_eq!(variant_array.value(4), Variant::Int32(34));
+
+        // Row 5: null (offset 2 in int_array, which has null)
+        assert!(variant_array.is_null(5));
+    }
+
+    #[test]
+    fn test_union_sparse_type_ids_row_builder() {
+        use arrow::array::{Int32Array, StringArray, UnionArray};
+        use arrow::buffer::ScalarBuffer;
+        use arrow::datatypes::{DataType, Field, UnionFields};
+        use std::sync::Arc;
+
+        // Create a sparse union with non-contiguous type IDs (1, 3)
+        let int_array = Int32Array::from(vec![Some(42), None]);
+        let string_array = StringArray::from(vec![None, Some("test")]);
+        let type_ids = [1, 3].into_iter().collect::<ScalarBuffer<i8>>();
+
+        let union_fields = UnionFields::new(
+            vec![1, 3], // Non-contiguous type IDs
+            vec![
+                Field::new("int_field", DataType::Int32, false),
+                Field::new("string_field", DataType::Utf8, false),
+            ],
+        );
+
+        let children: Vec<Arc<dyn Array>> = vec![Arc::new(int_array), Arc::new(string_array)];
+
+        let union_array = UnionArray::try_new(
+            union_fields,
+            type_ids,
+            None, // Sparse union
+            children,
+        )
+        .unwrap();
+
+        // Test the row builder
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(union_array.data_type(), &union_array).unwrap();
+
+        let mut variant_builder = VariantArrayBuilder::new(union_array.len());
+        for i in 0..union_array.len() {
+            let mut builder = variant_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+        let variant_array = variant_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 2);
+
+        // Row 0: int 42 (type_id = 1)
+        assert_eq!(variant_array.value(0), Variant::Int32(42));
+
+        // Row 1: string "test" (type_id = 3)
+        assert_eq!(variant_array.value(1), Variant::from("test"));
+    }
+
+    #[test]
+    fn test_decimal32_row_builder() {
+        use arrow::array::Decimal32Array;
+        use parquet_variant::VariantDecimal4;
+
+        // Test Decimal32Array with scale 2 (e.g., for currency: 12.34)
+        let decimal_array = Decimal32Array::from(vec![Some(1234), None, Some(-5678)])
+            .with_precision_and_scale(9, 2)
+            .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(decimal_array.data_type(), &decimal_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        for i in 0..decimal_array.len() {
+            let mut builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+
+        // Row 0: 12.34 (1234 with scale 2)
+        assert_eq!(
+            variant_array.value(0),
+            Variant::from(VariantDecimal4::try_new(1234, 2).unwrap())
+        );
+
+        // Row 1: null
+        assert!(variant_array.is_null(1));
+
+        // Row 2: -56.78 (-5678 with scale 2)
+        assert_eq!(
+            variant_array.value(2),
+            Variant::from(VariantDecimal4::try_new(-5678, 2).unwrap())
+        );
+    }
+
+    #[test]
+    fn test_decimal128_row_builder() {
+        use arrow::array::Decimal128Array;
+        use parquet_variant::VariantDecimal16;
+
+        // Test Decimal128Array with negative scale (multiply by 10^|scale|)
+        let decimal_array = Decimal128Array::from(vec![Some(123), None, Some(456)])
+            .with_precision_and_scale(10, -2)
+            .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(decimal_array.data_type(), &decimal_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        for i in 0..decimal_array.len() {
+            let mut builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+
+        // Row 0: 123 * 10^2 = 12300 with scale 0 (negative scale handling)
+        assert_eq!(
+            variant_array.value(0),
+            Variant::from(VariantDecimal16::try_new(12300, 0).unwrap())
+        );
+
+        // Row 1: null
+        assert!(variant_array.is_null(1));
+
+        // Row 2: 456 * 10^2 = 45600 with scale 0
+        assert_eq!(
+            variant_array.value(2),
+            Variant::from(VariantDecimal16::try_new(45600, 0).unwrap())
+        );
+    }
+
+    #[test]
+    fn test_decimal256_overflow_row_builder() {
+        use arrow::array::Decimal256Array;
+        use arrow::datatypes::i256;
+
+        // Test Decimal256Array with a value that overflows i128
+        let large_value = i256::from_i128(i128::MAX) + i256::from(1); // Overflows i128
+        let decimal_array = Decimal256Array::from(vec![Some(large_value), Some(i256::from(123))])
+            .with_precision_and_scale(76, 3)
+            .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(decimal_array.data_type(), &decimal_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(2);
+
+        for i in 0..decimal_array.len() {
+            let mut builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 2);
+
+        // Row 0: overflow value becomes Variant::Null
+        assert_eq!(variant_array.value(0), Variant::Null);
+
+        // Row 1: normal value converts successfully
+        assert_eq!(
+            variant_array.value(1),
+            Variant::from(VariantDecimal16::try_new(123, 3).unwrap())
+        );
+    }
+
+    #[test]
+    fn test_binary_row_builder() {
+        use arrow::array::BinaryArray;
+
+        // Test BinaryArray with various binary data
+        let binary_data = vec![
+            Some(b"hello".as_slice()),
+            None,
+            Some(b"\x00\x01\x02\xFF".as_slice()),
+            Some(b"".as_slice()), // Empty binary
+        ];
+        let binary_array = BinaryArray::from(binary_data);
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(binary_array.data_type(), &binary_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(4);
+
+        for i in 0..binary_array.len() {
+            let mut builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 4);
+
+        // Row 0: "hello" bytes
+        assert_eq!(variant_array.value(0), Variant::from(b"hello".as_slice()));
+
+        // Row 1: null
+        assert!(variant_array.is_null(1));
+
+        // Row 2: binary with special bytes
+        let bytes = [0x00, 0x01, 0x02, 0xFF];
+        assert_eq!(variant_array.value(2), Variant::from(bytes.as_slice()));
+
+        // Row 3: empty binary
+        let bytes = [];
+        assert_eq!(variant_array.value(3), Variant::from(bytes.as_slice()));
+    }
+
+    #[test]
+    fn test_large_binary_row_builder() {

Review Comment:
   Again, this test is probably redundant, now that I think about it...



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -20,6 +20,7 @@
 /// Convert the input array to a `VariantArray` row by row, using `method`
 /// not requiring a generic type to downcast the generic array to a specific
 /// array type and `cast_fn` to transform each element to a type compatible with Variant
+#[allow(unused)]

Review Comment:
   These macros are fully unused now that https://github.com/apache/arrow-rs/pull/8280 has merged, but I wasn't sure if they are useful to keep around "just in case" ?



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)

Review Comment:
   ```suggestion
           // Pre-cast keys to strings once
   ```



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr

Review Comment:
   Easiest to understand this macro by looking at its use sites below.
   
   The hope is that it accepts parameters that look intuitive and self-documenting at the use sites, while cleverly creating the code we actually want here.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),

Review Comment:
   This was almost redundant enough to make a macro for... but I don't think it would have saved much and it would have hurt readability.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(

Review Comment:
   Also -- the original code would just panic (`unwrap`) if the conversion failed, which seemed... less desirable.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,

Review Comment:
   NOTE: type ids are _not_ required to be dense, so we have to use a hashmap with explicit indices



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(

Review Comment:
   This isn't actually a lambda -- the macro inlines the "body" -- so it can return a `Result` here with no changes to the macro.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}

Review Comment:
   ```suggestion
           object_builder.finish();
   ```



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {

Review Comment:
   This was easily the most annoying part of the whole columnar/row-builder transformation -- there's technically not guarantee that subsequent `append_row` calls use sequentially increasing index values, so we have to handle jumps both forward and backward while hopefully being efficient in the common case that index values are sequential.



##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::VariantArrayBuilder;
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    #[test]
+    fn test_primitive_row_builder() {
+        // Test Int32Array
+        let int_array = Int32Array::from(vec![Some(42), None, Some(100)]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(int_array.data_type(), &int_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test null value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test second value
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::Int32(42));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::Int32(100));
+    }
+
+    #[test]
+    fn test_string_row_builder() {
+        let string_array = StringArray::from(vec![Some("hello"), None, Some("world")]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(string_array.data_type(), &string_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::from("hello"));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::from("world"));
+    }
+
+    #[test]
+    fn test_boolean_row_builder() {
+        let bool_array = BooleanArray::from(vec![Some(true), None, Some(false)]);
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(bool_array.data_type(), &bool_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+        assert_eq!(variant_array.value(0), Variant::from(true));
+        assert!(variant_array.is_null(1));
+        assert_eq!(variant_array.value(2), Variant::from(false));
+    }
+
+    #[test]
+    fn test_struct_row_builder() {
+        use arrow::array::{ArrayRef, Int32Array, StringArray, StructArray};
+        use arrow_schema::{DataType, Field};
+        use std::sync::Arc;
+
+        // Create a struct array with int and string fields
+        let int_field = Field::new("id", DataType::Int32, true);
+        let string_field = Field::new("name", DataType::Utf8, true);
+
+        let int_array = Int32Array::from(vec![Some(1), None, Some(3)]);
+        let string_array = StringArray::from(vec![Some("Alice"), Some("Bob"), None]);
+
+        let struct_array = StructArray::try_new(
+            vec![int_field, string_field].into(),
+            vec![
+                Arc::new(int_array) as ArrayRef,
+                Arc::new(string_array) as ArrayRef,
+            ],
+            None,
+        )
+        .unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(struct_array.data_type(), &struct_array).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(3);
+
+        // Test first row
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(0, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test second row (with null int field)
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(1, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        // Test third row (with null string field)
+        let mut variant_builder = array_builder.variant_builder();
+        row_builder.append_row(2, &mut variant_builder).unwrap();
+        variant_builder.finish();
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 3);
+
+        // Check first row - should have both fields
+        let first_variant = variant_array.value(0);
+        assert_eq!(first_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            first_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+
+        // Check second row - should have name field but not id (null field omitted)
+        let second_variant = variant_array.value(1);
+        assert_eq!(second_variant.get_object_field("id"), None); // null field omitted
+        assert_eq!(
+            second_variant.get_object_field("name"),
+            Some(Variant::from("Bob"))
+        );
+
+        // Check third row - should have id field but not name (null field omitted)
+        let third_variant = variant_array.value(2);
+        assert_eq!(third_variant.get_object_field("id"), Some(Variant::from(3)));
+        assert_eq!(third_variant.get_object_field("name"), None); // null field omitted
+    }
+
+    #[test]
+    fn test_run_end_encoded_row_builder() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array: [A, A, B, B, B, C]
+        // run_ends: [2, 5, 6]
+        // values: ["A", "B", "C"]
+        let values = StringArray::from(vec!["A", "B", "C"]);
+        let run_ends = Int32Array::from(vec![2, 5, 6]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(6);
+
+        // Test sequential access (most common case)
+        for i in 0..6 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 6);
+
+        // Verify the values
+        assert_eq!(variant_array.value(0), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(1), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(2), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(3), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(4), Variant::from("B")); // Run 1
+        assert_eq!(variant_array.value(5), Variant::from("C")); // Run 2
+    }
+
+    #[test]
+    fn test_run_end_encoded_random_access() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array: [A, A, B, B, B, C]
+        let values = StringArray::from(vec!["A", "B", "C"]);
+        let run_ends = Int32Array::from(vec![2, 5, 6]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+
+        // Test random access pattern (backward jumps, forward jumps)
+        let access_pattern = [0, 5, 2, 4, 1, 3]; // Mix of all cases
+        let expected_values = ["A", "C", "B", "B", "A", "B"];
+
+        for (i, &index) in access_pattern.iter().enumerate() {
+            let mut array_builder = VariantArrayBuilder::new(1);
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(index, &mut variant_builder).unwrap();
+            variant_builder.finish();
+
+            let variant_array = array_builder.build();
+            assert_eq!(variant_array.value(0), Variant::from(expected_values[i]));
+        }
+    }
+
+    #[test]
+    fn test_run_end_encoded_with_nulls() {
+        use arrow::array::{Int32Array, RunArray};
+        use arrow::datatypes::Int32Type;
+
+        // Create a run-end encoded array with null values: [A, A, null, null, B]
+        let values = StringArray::from(vec![Some("A"), None, Some("B")]);
+        let run_ends = Int32Array::from(vec![2, 4, 5]);
+        let run_array = RunArray::<Int32Type>::try_new(&run_ends, &values).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(run_array.data_type(), &run_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values
+        assert_eq!(variant_array.value(0), Variant::from("A")); // Run 0
+        assert_eq!(variant_array.value(1), Variant::from("A")); // Run 0
+        assert!(variant_array.is_null(2)); // Run 1 (null)
+        assert!(variant_array.is_null(3)); // Run 1 (null)
+        assert_eq!(variant_array.value(4), Variant::from("B")); // Run 2
+    }
+
+    #[test]
+    fn test_dictionary_row_builder() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array: keys=[0, 1, 0, 2, 1], values=["apple", "banana", "cherry"]
+        let values = StringArray::from(vec!["apple", "banana", "cherry"]);
+        let keys = Int32Array::from(vec![0, 1, 0, 2, 1]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values match the dictionary lookup
+        assert_eq!(variant_array.value(0), Variant::from("apple")); // keys[0] = 0 -> values[0] = "apple"
+        assert_eq!(variant_array.value(1), Variant::from("banana")); // keys[1] = 1 -> values[1] = "banana"
+        assert_eq!(variant_array.value(2), Variant::from("apple")); // keys[2] = 0 -> values[0] = "apple"
+        assert_eq!(variant_array.value(3), Variant::from("cherry")); // keys[3] = 2 -> values[2] = "cherry"
+        assert_eq!(variant_array.value(4), Variant::from("banana")); // keys[4] = 1 -> values[1] = "banana"
+    }
+
+    #[test]
+    fn test_dictionary_with_nulls() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array with null keys: keys=[0, null, 1, null, 2], values=["x", "y", "z"]
+        let values = StringArray::from(vec!["x", "y", "z"]);
+        let keys = Int32Array::from(vec![Some(0), None, Some(1), None, Some(2)]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the values and nulls
+        assert_eq!(variant_array.value(0), Variant::from("x")); // keys[0] = 0 -> values[0] = "x"
+        assert!(variant_array.is_null(1)); // keys[1] = null
+        assert_eq!(variant_array.value(2), Variant::from("y")); // keys[2] = 1 -> values[1] = "y"
+        assert!(variant_array.is_null(3)); // keys[3] = null
+        assert_eq!(variant_array.value(4), Variant::from("z")); // keys[4] = 2 -> values[2] = "z"
+    }
+
+    #[test]
+    fn test_dictionary_random_access() {
+        use arrow::array::{DictionaryArray, Int32Array};
+        use arrow::datatypes::Int32Type;
+
+        // Create a dictionary array: keys=[0, 1, 2, 0, 1, 2], values=["red", "green", "blue"]
+        let values = StringArray::from(vec!["red", "green", "blue"]);
+        let keys = Int32Array::from(vec![0, 1, 2, 0, 1, 2]);
+        let dict_array = DictionaryArray::<Int32Type>::try_new(keys, Arc::new(values)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+
+        // Test random access pattern
+        let access_pattern = [5, 0, 3, 1, 4, 2]; // Random order
+        let expected_values = ["blue", "red", "red", "green", "green", "blue"];
+
+        for (i, &index) in access_pattern.iter().enumerate() {
+            let mut array_builder = VariantArrayBuilder::new(1);
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(index, &mut variant_builder).unwrap();
+            variant_builder.finish();
+
+            let variant_array = array_builder.build();
+            assert_eq!(variant_array.value(0), Variant::from(expected_values[i]));
+        }
+    }
+
+    #[test]
+    fn test_nested_dictionary() {
+        use arrow::array::{DictionaryArray, Int32Array, StructArray};
+        use arrow::datatypes::{Field, Int32Type};
+
+        // Create a dictionary with struct values
+        let id_array = Int32Array::from(vec![1, 2, 3]);
+        let name_array = StringArray::from(vec!["Alice", "Bob", "Charlie"]);
+        let struct_array = StructArray::from(vec![
+            (
+                Arc::new(Field::new("id", DataType::Int32, false)),
+                Arc::new(id_array) as ArrayRef,
+            ),
+            (
+                Arc::new(Field::new("name", DataType::Utf8, false)),
+                Arc::new(name_array) as ArrayRef,
+            ),
+        ]);
+
+        let keys = Int32Array::from(vec![0, 1, 0, 2, 1]);
+        let dict_array =
+            DictionaryArray::<Int32Type>::try_new(keys, Arc::new(struct_array)).unwrap();
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(dict_array.data_type(), &dict_array).unwrap();
+        let mut array_builder = VariantArrayBuilder::new(5);
+
+        // Test sequential access
+        for i in 0..5 {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(i, &mut variant_builder).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), 5);
+
+        // Verify the nested struct values
+        let first_variant = variant_array.value(0);
+        assert_eq!(first_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            first_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+
+        let second_variant = variant_array.value(1);
+        assert_eq!(
+            second_variant.get_object_field("id"),
+            Some(Variant::from(2))
+        );
+        assert_eq!(
+            second_variant.get_object_field("name"),
+            Some(Variant::from("Bob"))
+        );
+
+        // Test that repeated keys give same values
+        let third_variant = variant_array.value(2);
+        assert_eq!(third_variant.get_object_field("id"), Some(Variant::from(1)));
+        assert_eq!(
+            third_variant.get_object_field("name"),
+            Some(Variant::from("Alice"))
+        );
+    }
+
+    #[test]
+    fn test_list_row_builder() {
+        use arrow::array::ListArray;
+
+        // Create a list array: [[1, 2], [3, 4, 5], null, []]
+        let data = vec![
+            Some(vec![Some(1), Some(2)]),
+            Some(vec![Some(3), Some(4), Some(5)]),
+            None,
+            Some(vec![]),
+        ];
+        let list_array = ListArray::from_iter_primitive::<Int32Type, _, _>(data);
+
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(list_array.data_type(), &list_array).unwrap();
+        let mut variant_array_builder = VariantArrayBuilder::new(list_array.len());
+
+        for i in 0..list_array.len() {
+            let mut builder = variant_array_builder.variant_builder();
+            row_builder.append_row(i, &mut builder).unwrap();
+            builder.finish();
+        }
+
+        let variant_array = variant_array_builder.build();
+
+        // Verify results
+        assert_eq!(variant_array.len(), 4);
+
+        // Row 0: [1, 2]
+        let row0 = variant_array.value(0);
+        let list0 = row0.as_list().unwrap();
+        assert_eq!(list0.len(), 2);
+        assert_eq!(list0.get(0), Some(Variant::from(1)));
+        assert_eq!(list0.get(1), Some(Variant::from(2)));
+
+        // Row 1: [3, 4, 5]
+        let row1 = variant_array.value(1);
+        let list1 = row1.as_list().unwrap();
+        assert_eq!(list1.len(), 3);
+        assert_eq!(list1.get(0), Some(Variant::from(3)));
+        assert_eq!(list1.get(1), Some(Variant::from(4)));
+        assert_eq!(list1.get(2), Some(Variant::from(5)));
+
+        // Row 2: null
+        assert!(variant_array.is_null(2));
+
+        // Row 3: []
+        let row3 = variant_array.value(3);
+        let list3 = row3.as_list().unwrap();
+        assert_eq!(list3.len(), 0);
+    }
+
+    #[test]
+    fn test_large_list_row_builder() {

Review Comment:
   We might not need this test, since the same template builder handles both normal and large lists



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2334832862


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(

Review Comment:
   NOTE: Now that strict casting has been merged in, the error case is now handled in the builder itself -- not the value transform.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2334829602


##########
parquet-variant-compute/src/variant_array_builder.rs:
##########
@@ -199,9 +199,14 @@ pub struct VariantArrayVariantBuilder<'a> {
     metadata_offsets: &'a mut Vec<usize>,
     value_offsets: &'a mut Vec<usize>,
     nulls: &'a mut NullBufferBuilder,
+    is_null: bool,
 }
 
 impl VariantBuilderExt for VariantArrayVariantBuilder<'_> {
+    /// Appending NULL to a variant array produces an actual NULL value
+    fn append_null(&mut self) {

Review Comment:
   That is correct. The variant builder API is pretty broken in general if builder methods are invoked more than once, so IMO this isn't making it any worse.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2331825012


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,2424 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::decimal_to_variant_decimal;
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    pub fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        match self {
+            ArrowToVariantRowBuilder::Null(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Boolean(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveUInt64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::PrimitiveFloat64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal128(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Decimal256(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampSecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMillisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampMicrosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::TimestampNanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Date64(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Second(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time32Millisecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Microsecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Time64Nanosecond(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Binary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::BinaryView(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::FixedSizeBinary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeUtf8(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Utf8View(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::List(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::LargeList(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Struct(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Map(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Union(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::Dictionary(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt16(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt32(b) => b.append_row(index, builder),
+            ArrowToVariantRowBuilder::RunEndEncodedInt64(b) => b.append_row(index, builder),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    let builder = match data_type {
+        DataType::Null => ArrowToVariantRowBuilder::Null(NullArrowToVariantBuilder),
+        DataType::Boolean => {
+            ArrowToVariantRowBuilder::Boolean(BooleanArrowToVariantBuilder::new(array))
+        }
+        DataType::Int8 => {
+            ArrowToVariantRowBuilder::PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int16 => {
+            ArrowToVariantRowBuilder::PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int32 => {
+            ArrowToVariantRowBuilder::PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Int64 => {
+            ArrowToVariantRowBuilder::PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt8 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt16 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt32 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::UInt64 => {
+            ArrowToVariantRowBuilder::PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float16 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float32 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Float64 => {
+            ArrowToVariantRowBuilder::PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array))
+        }
+        DataType::Decimal32(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal64(_, scale) => {
+            ArrowToVariantRowBuilder::Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+        }
+        DataType::Decimal128(_, scale) => ArrowToVariantRowBuilder::Decimal128(
+            Decimal128ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Decimal256(_, scale) => ArrowToVariantRowBuilder::Decimal256(
+            Decimal256ArrowToVariantBuilder::new(array, *scale),
+        ),
+        DataType::Timestamp(time_unit, time_zone) => match time_unit {
+            TimeUnit::Second => ArrowToVariantRowBuilder::TimestampSecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Millisecond => ArrowToVariantRowBuilder::TimestampMillisecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Microsecond => ArrowToVariantRowBuilder::TimestampMicrosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+            TimeUnit::Nanosecond => ArrowToVariantRowBuilder::TimestampNanosecond(
+                TimestampArrowToVariantBuilder::new(array, time_zone.is_some()),
+            ),
+        },
+        DataType::Date32 => ArrowToVariantRowBuilder::Date32(DateArrowToVariantBuilder::new(array)),
+        DataType::Date64 => ArrowToVariantRowBuilder::Date64(DateArrowToVariantBuilder::new(array)),
+        DataType::Time32(time_unit) => match time_unit {
+            TimeUnit::Second => {
+                ArrowToVariantRowBuilder::Time32Second(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Millisecond => {
+                ArrowToVariantRowBuilder::Time32Millisecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time32 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Time64(time_unit) => match time_unit {
+            TimeUnit::Microsecond => {
+                ArrowToVariantRowBuilder::Time64Microsecond(TimeArrowToVariantBuilder::new(array))
+            }
+            TimeUnit::Nanosecond => {
+                ArrowToVariantRowBuilder::Time64Nanosecond(TimeArrowToVariantBuilder::new(array))
+            }
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported Time64 unit: {time_unit:?}"
+                )))
+            }
+        },
+        DataType::Duration(_) | DataType::Interval(_) => {
+            return Err(ArrowError::InvalidArgumentError(
+                "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                    .to_string(),
+            ))
+        }
+        DataType::Binary => {
+            ArrowToVariantRowBuilder::Binary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::LargeBinary => {
+            ArrowToVariantRowBuilder::LargeBinary(BinaryArrowToVariantBuilder::new(array))
+        }
+        DataType::BinaryView => {
+            ArrowToVariantRowBuilder::BinaryView(BinaryViewArrowToVariantBuilder::new(array))
+        }
+        DataType::FixedSizeBinary(_) => ArrowToVariantRowBuilder::FixedSizeBinary(
+            FixedSizeBinaryArrowToVariantBuilder::new(array),
+        ),
+        DataType::Utf8 => ArrowToVariantRowBuilder::Utf8(StringArrowToVariantBuilder::new(array)),
+        DataType::LargeUtf8 => {
+            ArrowToVariantRowBuilder::LargeUtf8(StringArrowToVariantBuilder::new(array))
+        }
+        DataType::Utf8View => {
+            ArrowToVariantRowBuilder::Utf8View(StringViewArrowToVariantBuilder::new(array))
+        }
+        DataType::List(_) => ArrowToVariantRowBuilder::List(ListArrowToVariantBuilder::new(array)?),
+        DataType::LargeList(_) => {
+            ArrowToVariantRowBuilder::LargeList(ListArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Struct(_) => {
+            ArrowToVariantRowBuilder::Struct(StructArrowToVariantBuilder::new(array.as_struct())?)
+        }
+        DataType::Map(_, _) => ArrowToVariantRowBuilder::Map(MapArrowToVariantBuilder::new(array)?),
+        DataType::Union(_, _) => {
+            ArrowToVariantRowBuilder::Union(UnionArrowToVariantBuilder::new(array)?)
+        }
+        DataType::Dictionary(_, _) => {
+            ArrowToVariantRowBuilder::Dictionary(DictionaryArrowToVariantBuilder::new(array)?)
+        }
+        DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+            DataType::Int16 => ArrowToVariantRowBuilder::RunEndEncodedInt16(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int32 => ArrowToVariantRowBuilder::RunEndEncodedInt32(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            DataType::Int64 => ArrowToVariantRowBuilder::RunEndEncodedInt64(
+                RunEndEncodedArrowToVariantBuilder::new(array)?,
+            ),
+            _ => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported run ends type: {:?}",
+                    run_ends.data_type()
+                )));
+            }
+        },
+        dt => {
+            return Err(ArrowError::CastError(format!(
+                "Unsupported data type for casting to Variant: {dt:?}",
+            )));
+        }
+    };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+/// Supports optional extra fields that are passed to the constructor.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $($bound:path)+)?>
+        $(where $where_path:path: $where_bound:path)?
+        $({ $($field:ident: $field_type:ty),* $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr },
+        |$value:ident| $value_transform:expr
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $($bound)+)?>
+        $(where $where_path: $where_bound)?
+        {
+            array: &$lifetime $array_type,
+            $($($field: $field_type,)*)?
+        }
+
+        impl<$lifetime $(, $generic: $($bound)+)?> $name<$lifetime $(, $generic)?>
+        $(where $where_path: $where_bound)?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $($field: $field_type),*)?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $($($field,)*)?
+                }
+            }
+
+            fn append_row(&self, index: usize, builder: &mut impl VariantBuilderExt) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    let $value = self.array.value(index);
+                    // Capture fields as variables the transform can access (hygiene)
+                    $($(let $field = &self.$field;)*)?
+                    builder.append_value($value_transform);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() },
+    |value| value
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| value
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        // Convert using Arrow's temporal conversion functions
+        let Some(naive_datetime) = as_datetime::<T>(value) else {
+            return Err(ArrowError::CastError(
+                "Failed to convert Arrow timestamp value to chrono::NaiveDateTime".to_string(),
+            ));
+        };
+        if *has_time_zone {
+            // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+            let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+            Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+        } else {
+            // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+            Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+        }
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where i64: From<T::Native>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value).map(Variant::from).unwrap_or(Variant::Null)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() },
+    |value| value
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() },
+    |value| value
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() },
+    |value| value
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        _index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(value_index, &mut list_builder)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(struct_array: &'a arrow::array::StructArray) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder =
+                make_arrow_to_variant_row_builder(field_array.data_type(), field_array.as_ref())?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(index, &mut field_builder)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once (like existing convert_map code)
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map (even if empty)
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(kv_index, &mut field_builder)?;
+        }
+
+        object_builder.finish(); // Empty map becomes empty object {}
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder =
+                make_arrow_to_variant_row_builder(child_array.data_type(), child_array.as_ref())?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(value_offset, builder)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(normalized_key, builder)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref())?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        index: usize,
+        builder: &mut impl VariantBuilderExt,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(self.run_number, builder)?;
+
+        Ok(())
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {

Review Comment:
   This was easily the most annoying part of the whole columnar/row-builder transformation -- there's technically no guarantee that subsequent `append_row` calls use sequentially increasing index values, so we have to handle jumps both forward and backward while hopefully being efficient in the common case that index values are sequential.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2334676236


##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -17,46 +17,18 @@
 
 //! Module for transforming a typed arrow `Array` to `VariantArray`.
 
-/// Convert the input array to a `VariantArray` row by row, using `method`
-/// not requiring a generic type to downcast the generic array to a specific
-/// array type and `cast_fn` to transform each element to a type compatible with Variant
-/// If `strict` is true(default), return error on conversion failure. If false, insert null.
-macro_rules! non_generic_conversion_array {
-    ($array:expr, $cast_fn:expr, $builder:expr) => {{
-        let array = $array;
-        for i in 0..array.len() {
-            if array.is_null(i) {
-                $builder.append_null();
-                continue;
-            }
-            let cast_value = $cast_fn(array.value(i));
-            $builder.append_variant(Variant::from(cast_value));
-        }
-    }};
-    ($array:expr, $cast_fn:expr, $builder:expr, $strict:expr) => {{
-        let array = $array;
-        for i in 0..array.len() {
-            if array.is_null(i) {
-                $builder.append_null();
-                continue;
-            }
-            match $cast_fn(array.value(i)) {
-                Some(cast_value) => {
-                    $builder.append_variant(Variant::from(cast_value));
-                }
-                None if $strict => {
-                    return Err(ArrowError::ComputeError(format!(
-                        "Failed to convert value at index {}: conversion failed",
-                        i
-                    )));
-                }
-                None => $builder.append_null(),
-            }
-        }
-        Ok::<(), ArrowError>(())
-    }};
+/// Options for controlling the behavior of `cast_to_variant_with_options`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CastOptions {

Review Comment:
   Moved from cast_to_variant.rs



##########
parquet-variant-compute/src/cast_to_variant.rs:
##########
@@ -15,131 +15,10 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use std::collections::HashMap;
-use std::sync::Arc;
-
-use crate::type_conversion::{
-    decimal_to_variant_decimal, generic_conversion_array, non_generic_conversion_array,
-    primitive_conversion_array, timestamp_to_variant_timestamp,
-};
-use crate::{VariantArray, VariantArrayBuilder};
-use arrow::array::{
-    Array, AsArray, OffsetSizeTrait, TimestampMicrosecondArray, TimestampMillisecondArray,
-    TimestampNanosecondArray, TimestampSecondArray,
-};
-use arrow::buffer::{OffsetBuffer, ScalarBuffer};
-use arrow::compute::kernels::cast;
-use arrow::datatypes::{
-    i256, ArrowNativeType, BinaryType, BinaryViewType, Date32Type, Date64Type, Decimal128Type,
-    Decimal256Type, Decimal32Type, Decimal64Type, Float16Type, Float32Type, Float64Type, Int16Type,
-    Int32Type, Int64Type, Int8Type, LargeBinaryType, RunEndIndexType, Time32MillisecondType,
-    Time32SecondType, Time64MicrosecondType, Time64NanosecondType, UInt16Type, UInt32Type,
-    UInt64Type, UInt8Type,
-};
-use arrow::temporal_conversions::{
-    timestamp_ms_to_datetime, timestamp_ns_to_datetime, timestamp_s_to_datetime,
-    timestamp_us_to_datetime,
-};
-use arrow_schema::{ArrowError, DataType, FieldRef, TimeUnit, UnionFields};
-use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
-use parquet_variant::{
-    Variant, VariantBuilder, VariantDecimal16, VariantDecimal4, VariantDecimal8,
-};
-
-/// Options for controlling the behavior of `cast_to_variant_with_options`.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CastOptions {
-    /// If true, return error on conversion failure. If false, insert null for failed conversions.
-    pub strict: bool,
-}
-
-impl Default for CastOptions {
-    fn default() -> Self {
-        Self { strict: true }
-    }
-}

Review Comment:
   Moved to type_conversions.rs



##########
parquet-variant/src/builder.rs:
##########
@@ -1758,6 +1771,34 @@ impl VariantBuilderExt for VariantBuilder {
     }
 }
 
+/// A [`VariantBuilderExt`] that inserts a new field into a variant object.
+pub struct ObjectFieldBuilder<'o, 'v, 's> {

Review Comment:
   Moved from from_json.rs



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "scovich (via GitHub)" <gi...@apache.org>.
scovich commented on PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#issuecomment-3272149499

   Attn @codephage2020 -- this PR conflicts with 
   * https://github.com/apache/arrow-rs/pull/8233
   
   Most of the code that other PR touched has been deleted by this PR, so I did my best to capture the intent in the new framework. 
   
   While resolving conflicts, I also had to expand the `define_row_builder!` macro to handle fallible conversions, which increased its complexity a bit. Hopefully still readable. The merge commit https://github.com/apache/arrow-rs/pull/8299/commits/26ae5c07a3f48ce00ebc793cfd8b6326fb459bfb actually makes it pretty easy to see what changed.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] [Variant] Implement row builders for cast_to_variant [arrow-rs]

Posted by "klion26 (via GitHub)" <gi...@apache.org>.
klion26 commented on code in PR #8299:
URL: https://github.com/apache/arrow-rs/pull/8299#discussion_r2347785006


##########
parquet-variant-compute/src/arrow_to_variant.rs:
##########
@@ -0,0 +1,1995 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::collections::HashMap;
+
+use crate::type_conversion::{decimal_to_variant_decimal, CastOptions};
+use arrow::array::{
+    Array, AsArray, GenericBinaryArray, GenericStringArray, OffsetSizeTrait, PrimitiveArray,
+};
+use arrow::compute::kernels::cast;
+use arrow::datatypes::{
+    ArrowNativeType, ArrowPrimitiveType, ArrowTemporalType, ArrowTimestampType, Date32Type,
+    Date64Type, Float16Type, Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type,
+    RunEndIndexType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
+    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
+    TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, UInt8Type,
+};
+use arrow::temporal_conversions::{as_date, as_datetime, as_time};
+use arrow_schema::{ArrowError, DataType, TimeUnit};
+use chrono::{DateTime, TimeZone, Utc};
+use parquet_variant::{
+    ObjectFieldBuilder, Variant, VariantBuilderExt, VariantDecimal16, VariantDecimal4,
+    VariantDecimal8,
+};
+
+// ============================================================================
+// Row-oriented builders for efficient Arrow-to-Variant conversion
+// ============================================================================
+
+/// Row builder for converting Arrow arrays to VariantArray row by row
+pub(crate) enum ArrowToVariantRowBuilder<'a> {
+    Null(NullArrowToVariantBuilder),
+    Boolean(BooleanArrowToVariantBuilder<'a>),
+    PrimitiveInt8(PrimitiveArrowToVariantBuilder<'a, Int8Type>),
+    PrimitiveInt16(PrimitiveArrowToVariantBuilder<'a, Int16Type>),
+    PrimitiveInt32(PrimitiveArrowToVariantBuilder<'a, Int32Type>),
+    PrimitiveInt64(PrimitiveArrowToVariantBuilder<'a, Int64Type>),
+    PrimitiveUInt8(PrimitiveArrowToVariantBuilder<'a, UInt8Type>),
+    PrimitiveUInt16(PrimitiveArrowToVariantBuilder<'a, UInt16Type>),
+    PrimitiveUInt32(PrimitiveArrowToVariantBuilder<'a, UInt32Type>),
+    PrimitiveUInt64(PrimitiveArrowToVariantBuilder<'a, UInt64Type>),
+    PrimitiveFloat16(PrimitiveArrowToVariantBuilder<'a, Float16Type>),
+    PrimitiveFloat32(PrimitiveArrowToVariantBuilder<'a, Float32Type>),
+    PrimitiveFloat64(PrimitiveArrowToVariantBuilder<'a, Float64Type>),
+    Decimal32(Decimal32ArrowToVariantBuilder<'a>),
+    Decimal64(Decimal64ArrowToVariantBuilder<'a>),
+    Decimal128(Decimal128ArrowToVariantBuilder<'a>),
+    Decimal256(Decimal256ArrowToVariantBuilder<'a>),
+    TimestampSecond(TimestampArrowToVariantBuilder<'a, TimestampSecondType>),
+    TimestampMillisecond(TimestampArrowToVariantBuilder<'a, TimestampMillisecondType>),
+    TimestampMicrosecond(TimestampArrowToVariantBuilder<'a, TimestampMicrosecondType>),
+    TimestampNanosecond(TimestampArrowToVariantBuilder<'a, TimestampNanosecondType>),
+    Date32(DateArrowToVariantBuilder<'a, Date32Type>),
+    Date64(DateArrowToVariantBuilder<'a, Date64Type>),
+    Time32Second(TimeArrowToVariantBuilder<'a, Time32SecondType>),
+    Time32Millisecond(TimeArrowToVariantBuilder<'a, Time32MillisecondType>),
+    Time64Microsecond(TimeArrowToVariantBuilder<'a, Time64MicrosecondType>),
+    Time64Nanosecond(TimeArrowToVariantBuilder<'a, Time64NanosecondType>),
+    Binary(BinaryArrowToVariantBuilder<'a, i32>),
+    LargeBinary(BinaryArrowToVariantBuilder<'a, i64>),
+    BinaryView(BinaryViewArrowToVariantBuilder<'a>),
+    FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder<'a>),
+    Utf8(StringArrowToVariantBuilder<'a, i32>),
+    LargeUtf8(StringArrowToVariantBuilder<'a, i64>),
+    Utf8View(StringViewArrowToVariantBuilder<'a>),
+    List(ListArrowToVariantBuilder<'a, i32>),
+    LargeList(ListArrowToVariantBuilder<'a, i64>),
+    Struct(StructArrowToVariantBuilder<'a>),
+    Map(MapArrowToVariantBuilder<'a>),
+    Union(UnionArrowToVariantBuilder<'a>),
+    Dictionary(DictionaryArrowToVariantBuilder<'a>),
+    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder<'a, Int16Type>),
+    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder<'a, Int32Type>),
+    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder<'a, Int64Type>),
+}
+
+impl<'a> ArrowToVariantRowBuilder<'a> {
+    /// Appends a single row at the given index to the supplied builder.
+    pub fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        use ArrowToVariantRowBuilder::*;
+        match self {
+            Null(b) => b.append_row(builder, index),
+            Boolean(b) => b.append_row(builder, index),
+            PrimitiveInt8(b) => b.append_row(builder, index),
+            PrimitiveInt16(b) => b.append_row(builder, index),
+            PrimitiveInt32(b) => b.append_row(builder, index),
+            PrimitiveInt64(b) => b.append_row(builder, index),
+            PrimitiveUInt8(b) => b.append_row(builder, index),
+            PrimitiveUInt16(b) => b.append_row(builder, index),
+            PrimitiveUInt32(b) => b.append_row(builder, index),
+            PrimitiveUInt64(b) => b.append_row(builder, index),
+            PrimitiveFloat16(b) => b.append_row(builder, index),
+            PrimitiveFloat32(b) => b.append_row(builder, index),
+            PrimitiveFloat64(b) => b.append_row(builder, index),
+            Decimal32(b) => b.append_row(builder, index),
+            Decimal64(b) => b.append_row(builder, index),
+            Decimal128(b) => b.append_row(builder, index),
+            Decimal256(b) => b.append_row(builder, index),
+            TimestampSecond(b) => b.append_row(builder, index),
+            TimestampMillisecond(b) => b.append_row(builder, index),
+            TimestampMicrosecond(b) => b.append_row(builder, index),
+            TimestampNanosecond(b) => b.append_row(builder, index),
+            Date32(b) => b.append_row(builder, index),
+            Date64(b) => b.append_row(builder, index),
+            Time32Second(b) => b.append_row(builder, index),
+            Time32Millisecond(b) => b.append_row(builder, index),
+            Time64Microsecond(b) => b.append_row(builder, index),
+            Time64Nanosecond(b) => b.append_row(builder, index),
+            Binary(b) => b.append_row(builder, index),
+            LargeBinary(b) => b.append_row(builder, index),
+            BinaryView(b) => b.append_row(builder, index),
+            FixedSizeBinary(b) => b.append_row(builder, index),
+            Utf8(b) => b.append_row(builder, index),
+            LargeUtf8(b) => b.append_row(builder, index),
+            Utf8View(b) => b.append_row(builder, index),
+            List(b) => b.append_row(builder, index),
+            LargeList(b) => b.append_row(builder, index),
+            Struct(b) => b.append_row(builder, index),
+            Map(b) => b.append_row(builder, index),
+            Union(b) => b.append_row(builder, index),
+            Dictionary(b) => b.append_row(builder, index),
+            RunEndEncodedInt16(b) => b.append_row(builder, index),
+            RunEndEncodedInt32(b) => b.append_row(builder, index),
+            RunEndEncodedInt64(b) => b.append_row(builder, index),
+        }
+    }
+}
+
+/// Factory function to create the appropriate row builder for a given DataType
+pub(crate) fn make_arrow_to_variant_row_builder<'a>(
+    data_type: &'a DataType,
+    array: &'a dyn Array,
+    options: &'a CastOptions,
+) -> Result<ArrowToVariantRowBuilder<'a>, ArrowError> {
+    use ArrowToVariantRowBuilder::*;
+    let builder =
+        match data_type {
+            DataType::Null => Null(NullArrowToVariantBuilder),
+            DataType::Boolean => Boolean(BooleanArrowToVariantBuilder::new(array)),
+            DataType::Int8 => PrimitiveInt8(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int16 => PrimitiveInt16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int32 => PrimitiveInt32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Int64 => PrimitiveInt64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt8 => PrimitiveUInt8(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt16 => PrimitiveUInt16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt32 => PrimitiveUInt32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::UInt64 => PrimitiveUInt64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float16 => PrimitiveFloat16(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float32 => PrimitiveFloat32(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Float64 => PrimitiveFloat64(PrimitiveArrowToVariantBuilder::new(array)),
+            DataType::Decimal32(_, scale) => {
+                Decimal32(Decimal32ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal64(_, scale) => {
+                Decimal64(Decimal64ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal128(_, scale) => {
+                Decimal128(Decimal128ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Decimal256(_, scale) => {
+                Decimal256(Decimal256ArrowToVariantBuilder::new(array, *scale))
+            }
+            DataType::Timestamp(time_unit, time_zone) => {
+                match time_unit {
+                    TimeUnit::Second => TimestampSecond(TimestampArrowToVariantBuilder::new(
+                        array,
+                        options,
+                        time_zone.is_some(),
+                    )),
+                    TimeUnit::Millisecond => TimestampMillisecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                    TimeUnit::Microsecond => TimestampMicrosecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                    TimeUnit::Nanosecond => TimestampNanosecond(
+                        TimestampArrowToVariantBuilder::new(array, options, time_zone.is_some()),
+                    ),
+                }
+            }
+            DataType::Date32 => Date32(DateArrowToVariantBuilder::new(array, options)),
+            DataType::Date64 => Date64(DateArrowToVariantBuilder::new(array, options)),
+            DataType::Time32(time_unit) => match time_unit {
+                TimeUnit::Second => Time32Second(TimeArrowToVariantBuilder::new(array, options)),
+                TimeUnit::Millisecond => {
+                    Time32Millisecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported Time32 unit: {time_unit:?}"
+                    )))
+                }
+            },
+            DataType::Time64(time_unit) => match time_unit {
+                TimeUnit::Microsecond => {
+                    Time64Microsecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                TimeUnit::Nanosecond => {
+                    Time64Nanosecond(TimeArrowToVariantBuilder::new(array, options))
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported Time64 unit: {time_unit:?}"
+                    )))
+                }
+            },
+            DataType::Duration(_) | DataType::Interval(_) => {
+                return Err(ArrowError::InvalidArgumentError(
+                    "Casting duration/interval types to Variant is not supported. \
+                 The Variant format does not define duration/interval types."
+                        .to_string(),
+                ))
+            }
+            DataType::Binary => Binary(BinaryArrowToVariantBuilder::new(array)),
+            DataType::LargeBinary => LargeBinary(BinaryArrowToVariantBuilder::new(array)),
+            DataType::BinaryView => BinaryView(BinaryViewArrowToVariantBuilder::new(array)),
+            DataType::FixedSizeBinary(_) => {
+                FixedSizeBinary(FixedSizeBinaryArrowToVariantBuilder::new(array))
+            }
+            DataType::Utf8 => Utf8(StringArrowToVariantBuilder::new(array)),
+            DataType::LargeUtf8 => LargeUtf8(StringArrowToVariantBuilder::new(array)),
+            DataType::Utf8View => Utf8View(StringViewArrowToVariantBuilder::new(array)),
+            DataType::List(_) => List(ListArrowToVariantBuilder::new(array, options)?),
+            DataType::LargeList(_) => LargeList(ListArrowToVariantBuilder::new(array, options)?),
+            DataType::Struct(_) => Struct(StructArrowToVariantBuilder::new(
+                array.as_struct(),
+                options,
+            )?),
+            DataType::Map(_, _) => Map(MapArrowToVariantBuilder::new(array, options)?),
+            DataType::Union(_, _) => Union(UnionArrowToVariantBuilder::new(array, options)?),
+            DataType::Dictionary(_, _) => {
+                Dictionary(DictionaryArrowToVariantBuilder::new(array, options)?)
+            }
+            DataType::RunEndEncoded(run_ends, _) => match run_ends.data_type() {
+                DataType::Int16 => {
+                    RunEndEncodedInt16(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                DataType::Int32 => {
+                    RunEndEncodedInt32(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                DataType::Int64 => {
+                    RunEndEncodedInt64(RunEndEncodedArrowToVariantBuilder::new(array, options)?)
+                }
+                _ => {
+                    return Err(ArrowError::CastError(format!(
+                        "Unsupported run ends type: {:?}",
+                        run_ends.data_type()
+                    )));
+                }
+            },
+            dt => {
+                return Err(ArrowError::CastError(format!(
+                    "Unsupported data type for casting to Variant: {dt:?}",
+                )));
+            }
+        };
+    Ok(builder)
+}
+
+/// Macro to define (possibly generic) row builders with consistent structure and behavior.
+///
+/// The macro optionally allows to define a transform for values read from the underlying
+/// array. Transforms of the form `|value| { ... }` are infallible (and should produce something
+/// that implements `Into<Variant>`), while transforms of the form `|value| -> Option<_> { ... }`
+/// are fallible (and should produce `Option<impl Into<Variant>>`); a failed tarnsform will either
+/// append null to the builder or return an error, depending on cast options.
+///
+/// Also supports optional extra fields that are passed to the constructor and which are available
+/// by reference in the value transform. Providing a fallible value transform requires also
+/// providing the extra field `options: &'a CastOptions`.
+// TODO: If/when the macro_metavar_expr feature stabilizes, the `ignore` meta-function would allow
+// us to "use" captured tokens without emitting them:
+//
+// ```
+// $(
+//     ${ignore($value)}
+//     $(
+//         ${ignore($option_ty)}
+//         options: &$lifetime CastOptions,
+//     )?
+// )?
+// ```
+//
+// That, in turn, would allow us to inject the `options` field whenever the user specifies a
+// fallible value transform, instead of requiring them to manually define it. This might not be
+// worth the trouble, tho, because it makes for some pretty bulky and unwieldy macro expansions.
+macro_rules! define_row_builder {
+    (
+        struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
+        $( where $where_path:path: $where_bound:path $(,)? )?
+        $({ $($field:ident: $field_type:ty),+ $(,)? })?,
+        |$array_param:ident| -> $array_type:ty { $init_expr:expr }
+        $(, |$value:ident| $(-> Option<$option_ty:ty>)? $value_transform:expr)?
+    ) => {
+        pub(crate) struct $name<$lifetime $(, $generic: $bound )?>
+        $( where $where_path: $where_bound )?
+        {
+            array: &$lifetime $array_type,
+            $( $( $field: $field_type, )+ )?
+        }
+
+        impl<$lifetime $(, $generic: $bound+ )?> $name<$lifetime $(, $generic)?>
+        $( where $where_path: $where_bound )?
+        {
+            pub(crate) fn new($array_param: &$lifetime dyn Array $(, $( $field: $field_type ),+ )?) -> Self {
+                Self {
+                    array: $init_expr,
+                    $( $( $field, )+ )?
+                }
+            }
+
+            fn append_row(&self, builder: &mut impl VariantBuilderExt, index: usize) -> Result<(), ArrowError> {
+                if self.array.is_null(index) {
+                    builder.append_null();
+                } else {
+                    // Macro hygiene: Give any extra fields names the value transform can access.
+                    //
+                    // The value transform doesn't normally reference cast options, but the macro's
+                    // caller still has to declare the field because stable rust has no way to "use"
+                    // a captured token without emitting it. So, silence unused variable warnings,
+                    // assuming that's the `options` field. Unfortunately, that also silences
+                    // legitimate compiler warnings if an infallible value transform fails to use
+                    // its first extra field.
+                    $(
+                        #[allow(unused)]
+                        $( let $field = &self.$field; )+
+                    )?
+
+                    // Apply the value transform, if any (with name swapping for hygiene)
+                    let value = self.array.value(index);
+                    $(
+                        let $value = value;
+                        let value = $value_transform;
+                        $(
+                            // NOTE: The `?` macro expansion fails without the type annotation.
+                            let Some(value): Option<$option_ty> = value else {
+                                if self.options.strict {
+                                    return Err(ArrowError::ComputeError(format!(
+                                        "Failed to convert value at index {index}: conversion failed",
+                                    )));
+                                } else {
+                                    builder.append_null();
+                                    return Ok(());
+                                }
+                            };
+                        )?
+                    )?
+                    builder.append_value(value);
+                }
+                Ok(())
+            }
+        }
+    };
+}
+
+define_row_builder!(
+    struct BooleanArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BooleanArray { array.as_boolean() }
+);
+
+define_row_builder!(
+    struct PrimitiveArrowToVariantBuilder<'a, T: ArrowPrimitiveType>
+    where T::Native: Into<Variant<'a, 'a>>,
+    |array| -> PrimitiveArray<T> { array.as_primitive() }
+);
+
+define_row_builder!(
+    struct Decimal32ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal32Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i32, VariantDecimal4)
+);
+
+define_row_builder!(
+    struct Decimal64ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal64Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i64, VariantDecimal8)
+);
+
+define_row_builder!(
+    struct Decimal128ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal128Array { array.as_primitive() },
+    |value| decimal_to_variant_decimal!(value, scale, i128, VariantDecimal16)
+);
+
+define_row_builder!(
+    struct Decimal256ArrowToVariantBuilder<'a> {
+        scale: i8,
+    },
+    |array| -> arrow::array::Decimal256Array { array.as_primitive() },
+    |value| {
+        // Decimal256 needs special handling - convert to i128 if possible
+        match value.to_i128() {
+            Some(i128_val) => decimal_to_variant_decimal!(i128_val, scale, i128, VariantDecimal16),
+            None => Variant::Null, // Value too large for i128
+        }
+    }
+);
+
+define_row_builder!(
+    struct TimestampArrowToVariantBuilder<'a, T: ArrowTimestampType> {
+        options: &'a CastOptions,
+        has_time_zone: bool,
+    },
+    |array| -> arrow::array::PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        // Convert using Arrow's temporal conversion functions
+        as_datetime::<T>(value).map(|naive_datetime| {
+            if *has_time_zone {
+                // Has timezone -> DateTime<Utc> -> TimestampMicros/TimestampNanos
+                let utc_dt: DateTime<Utc> = Utc.from_utc_datetime(&naive_datetime);
+                Variant::from(utc_dt) // Uses From<DateTime<Utc>> for Variant
+            } else {
+                // No timezone -> NaiveDateTime -> TimestampNtzMicros/TimestampNtzNanos
+                Variant::from(naive_datetime) // Uses From<NaiveDateTime> for Variant
+            }
+        })
+    }
+);
+
+define_row_builder!(
+    struct DateArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let date_value = i64::from(value);
+        as_date::<T>(date_value)
+    }
+);
+
+define_row_builder!(
+    struct TimeArrowToVariantBuilder<'a, T: ArrowTemporalType>
+    where
+        i64: From<T::Native>,
+    {
+        options: &'a CastOptions,
+    },
+    |array| -> PrimitiveArray<T> { array.as_primitive() },
+    |value| -> Option<_> {
+        let time_value = i64::from(value);
+        as_time::<T>(time_value)
+    }
+);
+
+define_row_builder!(
+    struct BinaryArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericBinaryArray<O> { array.as_binary() }
+);
+
+define_row_builder!(
+    struct BinaryViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::BinaryViewArray { array.as_byte_view() }
+);
+
+define_row_builder!(
+    struct FixedSizeBinaryArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::FixedSizeBinaryArray { array.as_fixed_size_binary() }
+);
+
+define_row_builder!(
+    struct StringArrowToVariantBuilder<'a, O: OffsetSizeTrait>,
+    |array| -> GenericStringArray<O> { array.as_string() }
+);
+
+define_row_builder!(
+    struct StringViewArrowToVariantBuilder<'a>,
+    |array| -> arrow::array::StringViewArray { array.as_string_view() }
+);
+
+/// Null builder that always appends null
+pub(crate) struct NullArrowToVariantBuilder;
+
+impl NullArrowToVariantBuilder {
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        _index: usize,
+    ) -> Result<(), ArrowError> {
+        builder.append_null();
+        Ok(())
+    }
+}
+
+/// Generic list builder for List and LargeList types
+pub(crate) struct ListArrowToVariantBuilder<'a, O: OffsetSizeTrait> {
+    list_array: &'a arrow::array::GenericListArray<O>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a, O: OffsetSizeTrait> ListArrowToVariantBuilder<'a, O> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let list_array = array.as_list();
+        let values = list_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            list_array,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.list_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.list_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        let mut list_builder = builder.try_new_list()?;
+        for value_index in start..end {
+            self.values_builder
+                .append_row(&mut list_builder, value_index)?;
+        }
+        list_builder.finish();
+        Ok(())
+    }
+}
+
+/// Struct builder for StructArray
+pub(crate) struct StructArrowToVariantBuilder<'a> {
+    struct_array: &'a arrow::array::StructArray,
+    field_builders: Vec<(&'a str, ArrowToVariantRowBuilder<'a>)>,
+}
+
+impl<'a> StructArrowToVariantBuilder<'a> {
+    pub(crate) fn new(
+        struct_array: &'a arrow::array::StructArray,
+        options: &'a CastOptions,
+    ) -> Result<Self, ArrowError> {
+        let mut field_builders = Vec::new();
+
+        // Create a row builder for each field
+        for (field_name, field_array) in struct_array
+            .column_names()
+            .iter()
+            .zip(struct_array.columns().iter())
+        {
+            let field_builder = make_arrow_to_variant_row_builder(
+                field_array.data_type(),
+                field_array.as_ref(),
+                options,
+            )?;
+            field_builders.push((*field_name, field_builder));
+        }
+
+        Ok(Self {
+            struct_array,
+            field_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.struct_array.is_null(index) {
+            builder.append_null();
+        } else {
+            // Create object builder for this struct row
+            let mut obj_builder = builder.try_new_object()?;
+
+            // Process each field
+            for (field_name, row_builder) in &mut self.field_builders {
+                let mut field_builder =
+                    parquet_variant::ObjectFieldBuilder::new(field_name, &mut obj_builder);
+                row_builder.append_row(&mut field_builder, index)?;
+            }
+
+            obj_builder.finish();
+        }
+        Ok(())
+    }
+}
+
+/// Map builder for MapArray types
+pub(crate) struct MapArrowToVariantBuilder<'a> {
+    map_array: &'a arrow::array::MapArray,
+    key_strings: arrow::array::StringArray,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> MapArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let map_array = array.as_map();
+
+        // Pre-cast keys to strings once
+        let keys = cast(map_array.keys(), &DataType::Utf8)?;
+        let key_strings = keys.as_string::<i32>().clone();
+
+        // Create recursive builder for values
+        let values = map_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            map_array,
+            key_strings,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        // Check for NULL map first (via null bitmap)
+        if self.map_array.is_null(index) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        let offsets = self.map_array.offsets();
+        let start = offsets[index].as_usize();
+        let end = offsets[index + 1].as_usize();
+
+        // Create object builder for this map
+        let mut object_builder = builder.try_new_object()?;
+
+        // Add each key-value pair (loop does nothing for empty maps - correct!)
+        for kv_index in start..end {
+            let key = self.key_strings.value(kv_index);
+            let mut field_builder = ObjectFieldBuilder::new(key, &mut object_builder);
+            self.values_builder
+                .append_row(&mut field_builder, kv_index)?;
+        }
+
+        object_builder.finish();
+        Ok(())
+    }
+}
+
+/// Union builder for both sparse and dense union arrays
+///
+/// NOTE: Union type ids are _not_ required to be dense, hence the hash map for child builders.
+pub(crate) struct UnionArrowToVariantBuilder<'a> {
+    union_array: &'a arrow::array::UnionArray,
+    child_builders: HashMap<i8, Box<ArrowToVariantRowBuilder<'a>>>,
+}
+
+impl<'a> UnionArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let union_array = array.as_union();
+        let type_ids = union_array.type_ids();
+
+        // Create child builders for each union field
+        let mut child_builders = HashMap::new();
+        for &type_id in type_ids {
+            let child_array = union_array.child(type_id);
+            let child_builder = make_arrow_to_variant_row_builder(
+                child_array.data_type(),
+                child_array.as_ref(),
+                options,
+            )?;
+            child_builders.insert(type_id, Box::new(child_builder));
+        }
+
+        Ok(Self {
+            union_array,
+            child_builders,
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        let type_id = self.union_array.type_id(index);
+        let value_offset = self.union_array.value_offset(index);
+
+        // Delegate to the appropriate child builder, or append null to handle an invalid type_id
+        match self.child_builders.get_mut(&type_id) {
+            Some(child_builder) => child_builder.append_row(builder, value_offset)?,
+            None => builder.append_null(),
+        }
+
+        Ok(())
+    }
+}
+
+/// Dictionary array builder with simple O(1) indexing
+pub(crate) struct DictionaryArrowToVariantBuilder<'a> {
+    keys: &'a dyn Array, // only needed for null checks
+    normalized_keys: Vec<usize>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+}
+
+impl<'a> DictionaryArrowToVariantBuilder<'a> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let dict_array = array.as_any_dictionary();
+        let values = dict_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        // WARNING: normalized_keys panics if values is empty
+        let normalized_keys = match values.len() {
+            0 => Vec::new(),
+            _ => dict_array.normalized_keys(),
+        };
+
+        Ok(Self {
+            keys: dict_array.keys(),
+            normalized_keys,
+            values_builder: Box::new(values_builder),
+        })
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        if self.keys.is_null(index) {
+            builder.append_null();
+        } else {
+            let normalized_key = self.normalized_keys[index];
+            self.values_builder.append_row(builder, normalized_key)?;
+        }
+        Ok(())
+    }
+}
+
+/// Run-end encoded array builder with efficient sequential access
+pub(crate) struct RunEndEncodedArrowToVariantBuilder<'a, R: RunEndIndexType> {
+    run_array: &'a arrow::array::RunArray<R>,
+    values_builder: Box<ArrowToVariantRowBuilder<'a>>,
+
+    run_ends: &'a [R::Native],
+    run_number: usize, // Physical index into run_ends and values
+    run_start: usize,  // Logical start index of current run
+}
+
+impl<'a, R: RunEndIndexType> RunEndEncodedArrowToVariantBuilder<'a, R> {
+    pub(crate) fn new(array: &'a dyn Array, options: &'a CastOptions) -> Result<Self, ArrowError> {
+        let Some(run_array) = array.as_run_opt() else {
+            return Err(ArrowError::CastError("Expected RunArray".to_string()));
+        };
+
+        let values = run_array.values();
+        let values_builder =
+            make_arrow_to_variant_row_builder(values.data_type(), values.as_ref(), options)?;
+
+        Ok(Self {
+            run_array,
+            values_builder: Box::new(values_builder),
+            run_ends: run_array.run_ends().values(),
+            run_number: 0,
+            run_start: 0,
+        })
+    }
+
+    fn set_run_for_index(&mut self, index: usize) -> Result<(), ArrowError> {
+        if index >= self.run_start {
+            let Some(run_end) = self.run_ends.get(self.run_number) else {
+                return Err(ArrowError::CastError(format!(
+                    "Index {index} beyond run array"
+                )));
+            };
+            if index < run_end.as_usize() {
+                return Ok(());
+            }
+            if index == run_end.as_usize() {
+                self.run_number += 1;
+                self.run_start = run_end.as_usize();
+                return Ok(());
+            }
+        }
+
+        // Use partition_point for all non-sequential cases
+        let run_number = self
+            .run_ends
+            .partition_point(|&run_end| run_end.as_usize() <= index);
+        if run_number >= self.run_ends.len() {
+            return Err(ArrowError::CastError(format!(
+                "Index {index} beyond run array"
+            )));
+        }
+        self.run_number = run_number;
+        self.run_start = match run_number {
+            0 => 0,
+            _ => self.run_ends[run_number - 1].as_usize(),
+        };
+        Ok(())
+    }
+
+    fn append_row(
+        &mut self,
+        builder: &mut impl VariantBuilderExt,
+        index: usize,
+    ) -> Result<(), ArrowError> {
+        self.set_run_for_index(index)?;
+
+        // Handle null values
+        if self.run_array.values().is_null(self.run_number) {
+            builder.append_null();
+            return Ok(());
+        }
+
+        // Re-encode the value
+        self.values_builder.append_row(builder, self.run_number)?;
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{VariantArray, VariantArrayBuilder};
+    use arrow::array::{ArrayRef, BooleanArray, Int32Array, StringArray};
+    use std::sync::Arc;
+
+    /// Builds a VariantArray from an Arrow array using the row builder.
+    fn execute_row_builder_test(array: &dyn Array) -> VariantArray {
+        let options = CastOptions::default();
+        let mut row_builder =
+            make_arrow_to_variant_row_builder(array.data_type(), array, &options).unwrap();
+
+        let mut array_builder = VariantArrayBuilder::new(array.len());
+
+        // The repetitive loop that appears in every test
+        for i in 0..array.len() {
+            let mut variant_builder = array_builder.variant_builder();
+            row_builder.append_row(&mut variant_builder, i).unwrap();
+            variant_builder.finish();
+        }
+
+        let variant_array = array_builder.build();
+        assert_eq!(variant_array.len(), array.len());
+        variant_array
+    }
+
+    /// Generic helper function to test row builders with basic assertion patterns.
+    /// Uses execute_row_builder_test and adds simple value comparison assertions.
+    fn test_row_builder_basic(array: &dyn Array, expected_values: Vec<Option<Variant>>) {
+        let variant_array = execute_row_builder_test(array);
+
+        // The repetitive assertion pattern
+        for (i, expected) in expected_values.iter().enumerate() {
+            match expected {
+                Some(variant) => {
+                    assert_eq!(variant_array.value(i), *variant, "Mismatch at index {}", i)
+                }
+                None => assert!(variant_array.is_null(i), "Expected null at index {}", i),
+            }
+        }
+    }
+
+    #[test]
+    fn test_primitive_row_builder() {

Review Comment:
   These added tests are very nice tests!



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: github-unsubscribe@arrow.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org