sdf-jkl commented on code in PR #10114:
URL: https://github.com/apache/arrow-rs/pull/10114#discussion_r4125140022


##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -705,6 +710,227 @@ pub(crate) fn variant_to_boolean(variant: &Variant<'_, 
'_>, shred: bool) -> Opti
     }
 }
 
+#[inline]
+fn write_float_to_string<F: Float>(f: F, out: &mut String) {
+    let mut buffer = ryu::Buffer::new();
+    out.push_str(buffer.format(f));
+}
+
+// Convert a variant to a borrowed string when possible, otherwise an owned 
formatted string.
+pub(crate) fn variant_to_string<'v>(
+    variant: &Variant<'_, 'v>,
+    formats: &TemporalFormats<'_>,
+) -> Option<Cow<'v, str>> {
+    match variant {
+        Variant::Null => None,
+        Variant::Object(_) => None,
+        Variant::String(s) => Some(Cow::Borrowed(s)),
+        Variant::ShortString(s) => Some(Cow::Borrowed(s.as_str())),
+        _ => {
+            let mut s = String::new();
+            write_variant_to_string(variant, formats, &mut 
s).then_some(Cow::Owned(s))
+        }
+    }
+}
+
+fn write_lexical_to_string<N: lexical_core::ToLexical>(out: &mut String, n: N) 
{
+    // i64::FORMATTED_SIZE is the upper bound for all integer types we support
+    // (i8/i16/i32/i64). With power-of-two feature it's 128, otherwise 20.
+    // We can't use N::FORMATTED_SIZE in a generic function 
(generic_const_exprs).
+    let mut buf = [0u8; i64::FORMATTED_SIZE];
+    let written = lexical_core::write(n, &mut buf);
+    // Lexical core produces valid UTF-8
+    out.push_str(unsafe { std::str::from_utf8_unchecked(written) });
+}
+
+fn write_variant_to_string(
+    variant: &Variant<'_, '_>,
+    formats: &TemporalFormats<'_>,
+    out: &mut String,
+) -> bool {
+    match variant {
+        Variant::Null => {
+            out.push_str(formats.null());
+            true
+        }
+        Variant::String(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::ShortString(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::BooleanTrue => {
+            out.push_str("true");
+            true
+        }
+        Variant::BooleanFalse => {
+            out.push_str("false");
+            true
+        }
+        Variant::Int8(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int16(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int32(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int64(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Float(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Double(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Decimal4(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true

Review Comment:
   Could we use `write_decimal` here and in the `Decimal8`/`Decimal16` arms? It 
was added in #11002 and is already available in this PR's base through 
`arrow::datatypes`. It formats the integer using a stack buffer and writes 
directly into `out`, avoiding the two temporary heap strings from 
`integer().to_string()` and `format_decimal_str()` for every decimal value.
   
   ```suggestion
               arrow::datatypes::write_decimal(out, d.integer(), d.scale() as 
_).is_ok()
   ```
   
   This snippet fits the current `bool` return type. If we adopt the separate 
`Result` suggestion, propagate the write result with `?` instead of `.is_ok()`.



##########
parquet-variant-compute/src/variant_to_arrow.rs:
##########
@@ -204,6 +205,62 @@ pub(crate) fn make_variant_to_arrow_row_builder<'a>(
     Ok(builder)
 }
 
+pub(crate) enum VariantToStringArrowRowBuilder<'a, B: StringLikeArrayBuilder> {
+    Get(VariantToStringGetArrowBuilder<'a, B>),
+    Shred(VariantToStringShredArrowBuilder<'a, B>),
+}

Review Comment:
   Could we follow the existing Boolean/numeric/timestamp/decimal builder 
pattern here: keep one string builder and one binary builder, each taking 
`shred: bool`, and drop the separate Get/Shred implementations and forwarding 
enums?
   
   For binary, the shared builder can select `value.as_u8_slice()` when 
shredding and `variant_to_binary(value)` otherwise. For strings, both modes can 
share the existing String/ShortString fast path; other values should only go 
through string formatting when `shred` is false.
   
   This preserves strict shredding behavior while removing the extra builder 
types, forwarding methods, and repeated constructor branches for the six 
string/binary array types.



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -705,6 +710,227 @@ pub(crate) fn variant_to_boolean(variant: &Variant<'_, 
'_>, shred: bool) -> Opti
     }
 }
 
+#[inline]
+fn write_float_to_string<F: Float>(f: F, out: &mut String) {
+    let mut buffer = ryu::Buffer::new();
+    out.push_str(buffer.format(f));
+}
+
+// Convert a variant to a borrowed string when possible, otherwise an owned 
formatted string.
+pub(crate) fn variant_to_string<'v>(
+    variant: &Variant<'_, 'v>,
+    formats: &TemporalFormats<'_>,
+) -> Option<Cow<'v, str>> {
+    match variant {
+        Variant::Null => None,
+        Variant::Object(_) => None,
+        Variant::String(s) => Some(Cow::Borrowed(s)),
+        Variant::ShortString(s) => Some(Cow::Borrowed(s.as_str())),
+        _ => {
+            let mut s = String::new();
+            write_variant_to_string(variant, formats, &mut 
s).then_some(Cow::Owned(s))
+        }
+    }
+}
+
+fn write_lexical_to_string<N: lexical_core::ToLexical>(out: &mut String, n: N) 
{
+    // i64::FORMATTED_SIZE is the upper bound for all integer types we support
+    // (i8/i16/i32/i64). With power-of-two feature it's 128, otherwise 20.
+    // We can't use N::FORMATTED_SIZE in a generic function 
(generic_const_exprs).
+    let mut buf = [0u8; i64::FORMATTED_SIZE];
+    let written = lexical_core::write(n, &mut buf);
+    // Lexical core produces valid UTF-8
+    out.push_str(unsafe { std::str::from_utf8_unchecked(written) });
+}
+
+fn write_variant_to_string(
+    variant: &Variant<'_, '_>,
+    formats: &TemporalFormats<'_>,
+    out: &mut String,
+) -> bool {
+    match variant {
+        Variant::Null => {
+            out.push_str(formats.null());
+            true
+        }
+        Variant::String(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::ShortString(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::BooleanTrue => {
+            out.push_str("true");
+            true
+        }
+        Variant::BooleanFalse => {
+            out.push_str("false");
+            true
+        }
+        Variant::Int8(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int16(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int32(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int64(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Float(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Double(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Decimal4(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Decimal8(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Decimal16(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Date(d) => {
+            // The writing is always success
+            write_temporal_display(out, d, formats.date()).is_ok()
+        }
+        Variant::Time(t) => {
+            // The writing is always success
+            write_temporal_display(out, t, formats.time()).is_ok()
+        }
+        Variant::TimestampMicros(t) => {
+            // The writing is always success
+            write_timestamp(
+                out,
+                t.naive_utc(),
+                "+00:00".parse().ok(),
+                formats.timestamp_tz(),
+            )
+            .is_ok()
+        }
+        Variant::TimestampNtzMicros(t) => {
+            // The writing is always success
+            write_timestamp(out, *t, None, formats.timestamp()).is_ok()
+        }
+        Variant::TimestampNanos(t) => {
+            // The writing is always success
+            write_timestamp(
+                out,
+                t.naive_utc(),
+                "+00:00".parse().ok(),
+                formats.timestamp_tz(),
+            )
+            .is_ok()
+        }
+        Variant::TimestampNtzNanos(t) => {
+            // The writing is always success
+            write_timestamp(out, *t, None, formats.timestamp()).is_ok()
+        }
+        Variant::Uuid(u) => write!(out, "{u}").is_ok(),
+        Variant::Binary(v) => match std::str::from_utf8(v) {
+            Ok(s) => {
+                out.push_str(s);
+                true
+            }
+            Err(_) => false,

Review Comment:
   Could we use Arrow's hex formatting for binary values nested in collections? 
A list containing `b"ab"` and `[0xff, 0x00]` currently becomes `[ab, ]`, 
whereas Arrow produces `[6162, ff00]`. Please add a comparison test for this 
case.



##########
parquet-variant-compute/src/variant_to_arrow.rs:
##########
@@ -1273,8 +1419,64 @@ macro_rules! define_variant_to_primitive_builder {
     }
 }
 
+pub(crate) struct VariantToStringGetArrowBuilder<'a, B: 
StringLikeArrayBuilder> {
+    builder: B,
+    cast_options: &'a CastOptions<'a>,
+    formats: TemporalFormats<'a>,
+}
+
+impl<'a, B: StringLikeArrayBuilder> VariantToStringGetArrowBuilder<'a, B> {
+    fn new(cast_options: &'a CastOptions<'a>, capacity: usize) -> Self {
+        Self {
+            builder: B::with_capacity(capacity),
+            cast_options,
+            formats: TemporalFormats::new(cast_options),
+        }
+    }
+
+    fn append_null(&mut self) -> Result<()> {
+        self.builder.append_null();
+        Ok(())
+    }
+
+    fn append_value(&mut self, value: &Variant<'_, '_>) -> Result<bool> {
+        let value = match value {
+            // short-circuit the common case of string-like variants to avoid 
the overhead of casting
+            Variant::String(value) => value,
+            Variant::ShortString(value) => value.as_str(),
+            _ => return self.append_cast_value(value),
+        };
+
+        self.builder.append_value(value);
+        Ok(true)
+    }
+
+    fn append_cast_value(&mut self, value: &Variant<'_, '_>) -> Result<bool> {
+        match variant_cast_with_options(value, self.cast_options, |value| {
+            variant_to_string(value, &self.formats)

Review Comment:
   A perf suggestion, can bench before and after.
   
   We can have a reusable `scratch: String` on the string builder. 
`variant_to_string` currently creates a fresh `String` for each formatted 
value, which is then copied into the Arrow builder and dropped.
   
   Calling `scratch.clear()` before each conversion, writing through 
`write_variant_to_string`, and appending `scratch.as_str()` on success would 
reuse the capacity across rows. The String/ShortString fast path can stay 
as-is. We would need to make the writer `pub(crate)` and preserve the wrapper's 
rejection of top-level Null/Object values (and the strict shredding guard if 
the builders are consolidated).



##########
parquet-variant-compute/src/variant_to_arrow.rs:
##########
@@ -1204,16 +1302,61 @@ impl VariantPathRowBuilder<'_> {
     }
 }
 
+#[derive(Default)]
+pub(crate) struct TemporalFormats<'a> {
+    date: CompiledTimeFormat<'a>,
+    time: CompiledTimeFormat<'a>,
+    timestamp: CompiledTimeFormat<'a>,
+    timestamp_tz: CompiledTimeFormat<'a>,
+    null: &'a str,
+}

Review Comment:
   Optional cleanup: could we move `TemporalFormats` beside the formatting 
functions in `type_conversion.rs`? That would remove the reverse dependency 
from the conversion module to the builder module and let the formatter access 
these fields directly, without the five forwarding getters.



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -705,6 +710,227 @@ pub(crate) fn variant_to_boolean(variant: &Variant<'_, 
'_>, shred: bool) -> Opti
     }
 }
 
+#[inline]
+fn write_float_to_string<F: Float>(f: F, out: &mut String) {
+    let mut buffer = ryu::Buffer::new();
+    out.push_str(buffer.format(f));
+}
+
+// Convert a variant to a borrowed string when possible, otherwise an owned 
formatted string.
+pub(crate) fn variant_to_string<'v>(
+    variant: &Variant<'_, 'v>,
+    formats: &TemporalFormats<'_>,
+) -> Option<Cow<'v, str>> {
+    match variant {
+        Variant::Null => None,
+        Variant::Object(_) => None,
+        Variant::String(s) => Some(Cow::Borrowed(s)),
+        Variant::ShortString(s) => Some(Cow::Borrowed(s.as_str())),
+        _ => {
+            let mut s = String::new();
+            write_variant_to_string(variant, formats, &mut 
s).then_some(Cow::Owned(s))
+        }
+    }
+}
+
+fn write_lexical_to_string<N: lexical_core::ToLexical>(out: &mut String, n: N) 
{
+    // i64::FORMATTED_SIZE is the upper bound for all integer types we support
+    // (i8/i16/i32/i64). With power-of-two feature it's 128, otherwise 20.
+    // We can't use N::FORMATTED_SIZE in a generic function 
(generic_const_exprs).
+    let mut buf = [0u8; i64::FORMATTED_SIZE];
+    let written = lexical_core::write(n, &mut buf);
+    // Lexical core produces valid UTF-8
+    out.push_str(unsafe { std::str::from_utf8_unchecked(written) });
+}
+
+fn write_variant_to_string(
+    variant: &Variant<'_, '_>,
+    formats: &TemporalFormats<'_>,
+    out: &mut String,
+) -> bool {
+    match variant {
+        Variant::Null => {
+            out.push_str(formats.null());
+            true
+        }
+        Variant::String(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::ShortString(s) => {
+            out.push_str(s);
+            true

Review Comment:
   Could we honor `FormatOptions::with_quoted_strings(true)` when formatting 
nested strings? These branches always write raw text, so lists containing 
commas, quotes, or newlines differ from Arrow. Please pass the option through 
and add a comparison test.



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -705,6 +710,227 @@ pub(crate) fn variant_to_boolean(variant: &Variant<'_, 
'_>, shred: bool) -> Opti
     }
 }
 
+#[inline]
+fn write_float_to_string<F: Float>(f: F, out: &mut String) {
+    let mut buffer = ryu::Buffer::new();
+    out.push_str(buffer.format(f));
+}
+
+// Convert a variant to a borrowed string when possible, otherwise an owned 
formatted string.
+pub(crate) fn variant_to_string<'v>(
+    variant: &Variant<'_, 'v>,
+    formats: &TemporalFormats<'_>,
+) -> Option<Cow<'v, str>> {
+    match variant {
+        Variant::Null => None,
+        Variant::Object(_) => None,
+        Variant::String(s) => Some(Cow::Borrowed(s)),
+        Variant::ShortString(s) => Some(Cow::Borrowed(s.as_str())),
+        _ => {
+            let mut s = String::new();
+            write_variant_to_string(variant, formats, &mut 
s).then_some(Cow::Owned(s))
+        }
+    }
+}
+
+fn write_lexical_to_string<N: lexical_core::ToLexical>(out: &mut String, n: N) 
{
+    // i64::FORMATTED_SIZE is the upper bound for all integer types we support
+    // (i8/i16/i32/i64). With power-of-two feature it's 128, otherwise 20.
+    // We can't use N::FORMATTED_SIZE in a generic function 
(generic_const_exprs).
+    let mut buf = [0u8; i64::FORMATTED_SIZE];
+    let written = lexical_core::write(n, &mut buf);
+    // Lexical core produces valid UTF-8
+    out.push_str(unsafe { std::str::from_utf8_unchecked(written) });
+}
+
+fn write_variant_to_string(
+    variant: &Variant<'_, '_>,
+    formats: &TemporalFormats<'_>,
+    out: &mut String,
+) -> bool {
+    match variant {
+        Variant::Null => {
+            out.push_str(formats.null());
+            true
+        }
+        Variant::String(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::ShortString(s) => {
+            out.push_str(s);
+            true
+        }
+        Variant::BooleanTrue => {
+            out.push_str("true");
+            true
+        }
+        Variant::BooleanFalse => {
+            out.push_str("false");
+            true
+        }
+        Variant::Int8(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int16(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int32(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Int64(i) => {
+            write_lexical_to_string(out, *i);
+            true
+        }
+        Variant::Float(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Double(f) => {
+            write_float_to_string(*f, out);
+            true
+        }
+        Variant::Decimal4(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Decimal8(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Decimal16(d) => {
+            let value_str = d.integer().to_string();
+            out.push_str(&format_decimal_str(
+                &value_str,
+                value_str.len(),
+                d.scale() as _,
+            ));
+            true
+        }
+        Variant::Date(d) => {
+            // The writing is always success
+            write_temporal_display(out, d, formats.date()).is_ok()
+        }
+        Variant::Time(t) => {
+            // The writing is always success
+            write_temporal_display(out, t, formats.time()).is_ok()
+        }
+        Variant::TimestampMicros(t) => {
+            // The writing is always success
+            write_timestamp(
+                out,
+                t.naive_utc(),
+                "+00:00".parse().ok(),
+                formats.timestamp_tz(),
+            )
+            .is_ok()
+        }
+        Variant::TimestampNtzMicros(t) => {
+            // The writing is always success
+            write_timestamp(out, *t, None, formats.timestamp()).is_ok()
+        }
+        Variant::TimestampNanos(t) => {
+            // The writing is always success
+            write_timestamp(
+                out,
+                t.naive_utc(),
+                "+00:00".parse().ok(),
+                formats.timestamp_tz(),
+            )
+            .is_ok()
+        }
+        Variant::TimestampNtzNanos(t) => {
+            // The writing is always success
+            write_timestamp(out, *t, None, formats.timestamp()).is_ok()
+        }
+        Variant::Uuid(u) => write!(out, "{u}").is_ok(),
+        Variant::Binary(v) => match std::str::from_utf8(v) {
+            Ok(s) => {
+                out.push_str(s);
+                true
+            }
+            Err(_) => false,
+        },
+        Variant::List(l) => {
+            write_list_to_string(l.iter(), formats, out);
+            true
+        }
+        Variant::Object(o) => {
+            write_map_to_string(o.iter(), formats, out);
+            true
+        }
+    }
+}
+
+fn write_list_to_string<'m, 'v>(
+    mut iter: impl Iterator<Item = Variant<'m, 'v>>,
+    formats: &TemporalFormats,
+    out: &mut String,
+) {
+    out.push('[');
+    if let Some(item) = iter.next()
+        && !write_variant_to_string(&item, formats, out)
+    {
+        out.push_str(formats.null());

Review Comment:
   Could we return `Result` from the recursive formatter and use `?`, rather 
than reducing formatting errors to `bool` and replacing failed children with 
null text? A date inside a list with `%Y-%m-%d %H` currently succeeds even with 
`safe: false`; the same format on a top-level date returns NULL with `safe: 
true`, while Arrow errors in both cases. Keeping formatting failures distinct 
from unsupported casts would fix both cases and remove the repeated 
`true`/`.is_ok()` branches. Please cover both cases with regression tests.



##########
arrow-cast/src/display.rs:
##########
@@ -817,6 +831,54 @@ timestamp_display!(
     TimestampNanosecondType
 );
 
+/// A temporal value that can be formatted with a [`CompiledTimeFormat`].
+///
+/// This trait is implemented for [`NaiveDate`], [`NaiveTime`], and 
[`NaiveDateTime`].
+/// It enables [`write_temporal_display`] to format these types according to a 
specified time format.
+pub trait TemporalFormat: Debug {
+    /// Writes this value using the given compiled time format.
+    fn write_with_time_format(
+        &self,
+        f: &mut dyn Write,
+        format: &CompiledTimeFormat<'_>,
+    ) -> FormatResult;
+}
+
+macro_rules! impl_temporal_format {
+    ($($t:ty),+ $(,)?) => {
+        $(
+            impl TemporalFormat for $t {
+                #[inline]
+                fn write_with_time_format(
+                    &self,
+                    f: &mut dyn Write,
+                    format: &CompiledTimeFormat<'_>,
+                ) -> FormatResult {
+                    match &format.0 {
+                        CompiledTimeFormatInner::Custom(items) => {
+                            write!(f, "{}", 
self.format_with_items(items.0.iter()))?

Review Comment:
   Could we use `self.format_with_items(items.0.iter()).write_to(f)?` here, and 
the equivalent in the custom timestamp branches? Chrono's `Display` 
implementation allocates an intermediate String even when the destination 
buffer is reused; `write_to` writes directly to it. It can leave partial output 
on failure, so we should propagate the error and cover invalid formats, as 
discussed in the error-handling comment.



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -762,4 +988,580 @@ macro_rules! primitive_conversion_single_value {
         )
     }};
 }
+use crate::variant_to_arrow::TemporalFormats;
 pub(crate) use primitive_conversion_single_value;
+
+#[cfg(test)]
+mod tests {
+    use crate::type_conversion::variant_to_string;
+    use crate::variant_to_arrow::TemporalFormats;
+    use arrow::array::{
+        Array, AsArray, BooleanArray, Date32Array, Float32Array, Float64Array, 
Int32Builder,
+        ListBuilder, MapBuilder, StringBuilder, Time64MicrosecondArray, 
TimestampMicrosecondArray,
+        TimestampNanosecondArray,
+    };
+    use arrow::compute::{CastOptions, cast, cast_with_options};
+    use arrow::util::display::FormatOptions;
+    use arrow_schema::DataType;
+    use chrono::{DateTime, NaiveDate, NaiveTime};
+    use parquet_variant::{Variant, VariantBuilder, VariantBuilderExt};
+    use std::borrow::Cow;
+    use std::iter::zip;
+
+    #[test]
+    fn test_compatible_cast_logic_with_cast_kernel() {

Review Comment:
   Optional test cleanup: could we factor the repeated comparison setup into a 
helper that runs `cast_to_variant` -> `variant_get` and compares against Arrow 
casting? Reusing it for Utf8/LargeUtf8/Utf8View, nulls, and safe/strict 
failures would shorten these tests while covering the builder path as well as 
the formatter.



##########
parquet-variant-compute/src/type_conversion.rs:
##########
@@ -705,6 +710,227 @@ pub(crate) fn variant_to_boolean(variant: &Variant<'_, 
'_>, shred: bool) -> Opti
     }
 }
 
+#[inline]
+fn write_float_to_string<F: Float>(f: F, out: &mut String) {
+    let mut buffer = ryu::Buffer::new();
+    out.push_str(buffer.format(f));
+}
+
+// Convert a variant to a borrowed string when possible, otherwise an owned 
formatted string.
+pub(crate) fn variant_to_string<'v>(
+    variant: &Variant<'_, 'v>,
+    formats: &TemporalFormats<'_>,
+) -> Option<Cow<'v, str>> {

Review Comment:
   If we adopt the scratch-buffer suggestion, could we remove this allocating 
wrapper and its `Cow` machinery? The builder can use `write_variant_to_string` 
directly, retaining the string fast path and top-level Null/Object checks. The 
comparison tests could exercise the public conversion path instead.
   
   For top-level Binary in Get mode, `std::str::from_utf8(bytes)` can also 
supply a borrowed `&str` directly to the builder, avoiding the extra copy 
through scratch. Invalid UTF-8 should retain safe/strict handling, shredding 
should still reject binary as a string, and nested binary should use Arrow's 
hex formatting.



##########
parquet-variant-compute/src/variant_to_arrow.rs:
##########
@@ -1204,16 +1302,61 @@ impl VariantPathRowBuilder<'_> {
     }
 }
 
+#[derive(Default)]
+pub(crate) struct TemporalFormats<'a> {
+    date: CompiledTimeFormat<'a>,
+    time: CompiledTimeFormat<'a>,
+    timestamp: CompiledTimeFormat<'a>,
+    timestamp_tz: CompiledTimeFormat<'a>,
+    null: &'a str,
+}
+
+impl<'a> TemporalFormats<'a> {
+    pub(crate) fn new(options: &CastOptions<'a>) -> Self {
+        let options = &options.format_options;
+        Self {
+            date: CompiledTimeFormat::new(options.date_format()),
+            time: CompiledTimeFormat::new(options.time_format()),
+            timestamp: CompiledTimeFormat::new(options.timestamp_format()),
+            timestamp_tz: 
CompiledTimeFormat::new(options.timestamp_tz_format()),
+            null: options.null(),
+        }
+    }
+
+    pub(crate) fn date(&self) -> &CompiledTimeFormat<'a> {
+        &self.date
+    }
+
+    pub(crate) fn time(&self) -> &CompiledTimeFormat<'a> {
+        &self.time
+    }
+
+    pub(crate) fn timestamp(&self) -> &CompiledTimeFormat<'a> {
+        &self.timestamp
+    }
+
+    pub(crate) fn timestamp_tz(&self) -> &CompiledTimeFormat<'a> {
+        &self.timestamp_tz
+    }
+
+    pub(crate) fn null(&self) -> &str {
+        self.null
+    }
+}
+
+// Define a builder for converting variant values into a primitive Arrow array.
+// `, $shred` passes shredding state; `; temporal_formats: $name` binds the 
temporal formats.
 macro_rules! define_variant_to_primitive_builder {
     (struct $name:ident<$lifetime:lifetime $(, $generic:ident: $bound:path )?>
     |$array_param:ident $(, $field:ident: $field_type:ty)?| -> 
$builder_name:ident $(< $array_type:ty >)? { $init_expr: expr },
-    |$value: ident $(, $shred: ident)?| $value_transform:expr,
+    |$value: ident $(, $shred: ident)? $(; temporal_formats: $formats:ident)?| 
$value_transform:expr,

Review Comment:
   Can we remove the `temporal_formats` macro parameter and its generated 
field, initialization, and local binding? No invocation uses it now that the 
string builder is handwritten and owns its formats directly.



-- 
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: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to