This is an automated email from the ASF dual-hosted git repository.

rich7420 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git


The following commit(s) were added to refs/heads/main by this push:
     new 214b02dcb [Feature][QDP]ParquetReader generic over f32/f64 with Arrow 
cast (#1393)
214b02dcb is described below

commit 214b02dcb7b32412246c0fa98d3b02cfeb1e7872
Author: ChenChen Lai <[email protected]>
AuthorDate: Thu Jun 11 17:11:06 2026 +0900

    [Feature][QDP]ParquetReader generic over f32/f64 with Arrow cast (#1393)
    
    * ParquetReader generic over f32/f64 with Arrow cast
    
    * address comment
---
 qdp/qdp-core/src/lib.rs             |   2 +-
 qdp/qdp-core/src/reader.rs          |  82 ++++--
 qdp/qdp-core/src/readers/parquet.rs | 481 ++++++++++++++++++++++--------------
 qdp/qdp-core/src/remote.rs          |   2 +-
 qdp/qdp-core/tests/parquet_f32.rs   | 297 ++++++++++++++++++++++
 5 files changed, 658 insertions(+), 206 deletions(-)

diff --git a/qdp/qdp-core/src/lib.rs b/qdp/qdp-core/src/lib.rs
index ac5dd5fe9..822fba96e 100644
--- a/qdp/qdp-core/src/lib.rs
+++ b/qdp/qdp-core/src/lib.rs
@@ -37,7 +37,7 @@ mod profiling;
 
 pub use error::{MahoutError, Result, cuda_error_to_string};
 pub use gpu::memory::Precision;
-pub use reader::{FloatElem, NullHandling, handle_float64_nulls};
+pub use reader::{FloatElem, NullHandling, handle_float32_nulls, 
handle_float64_nulls};
 pub use types::{Dtype, Encoding};
 
 // Throughput/latency pipeline runner: single path using QdpEngine and 
encode_batch in Rust.
diff --git a/qdp/qdp-core/src/reader.rs b/qdp/qdp-core/src/reader.rs
index a51fd334a..46db0a6c1 100644
--- a/qdp/qdp-core/src/reader.rs
+++ b/qdp/qdp-core/src/reader.rs
@@ -45,20 +45,39 @@
 //! }
 //! ```
 
-use arrow::array::{Array, Float64Array};
+use arrow::array::{Array, Float32Array, Float64Array, PrimitiveArray};
+use arrow::datatypes::{ArrowPrimitiveType, Float32Type, Float64Type};
 
-use crate::error::Result;
+use crate::error::{MahoutError, Result};
+
+/// Maps a Rust float primitive to its Arrow array type.
+///
+/// `pub(crate)` seals `FloatElem`: external callers cannot implement 
`ArrowPrimitive`
+/// and therefore cannot implement `FloatElem` for new types.
+pub(crate) trait ArrowPrimitive {
+    type ArrowType: ArrowPrimitiveType<Native = Self>;
+}
+
+impl ArrowPrimitive for f32 {
+    type ArrowType = Float32Type;
+}
+
+impl ArrowPrimitive for f64 {
+    type ArrowType = Float64Type;
+}
 
 /// Scalar element type for [`DataReader`] output (`f32` or `f64` only).
 ///
-/// Keeps f32 file data as `Vec<f32>` end-to-end once readers implement
-/// `DataReader<f32>`; today most readers use the default `T = f64`.
-pub trait FloatElem: Copy + Send + Sync + 'static {}
+/// Sealed by the `pub(crate) ArrowPrimitive` supertrait — no external 
implementations
+/// are possible. Keeps f32 file data as `Vec<f32>` end-to-end; today most 
readers
+/// use the default `T = f64`.
+#[allow(private_bounds)]
+pub trait FloatElem: ArrowPrimitive + Copy + Default + Send + Sync + 'static {}
 
 impl FloatElem for f32 {}
 impl FloatElem for f64 {}
 
-/// Policy for handling null values in Float64 arrays.
+/// Policy for handling null values in float arrays.
 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
 pub enum NullHandling {
     /// Replace nulls with 0.0 (backward-compatible default).
@@ -68,34 +87,59 @@ pub enum NullHandling {
     Reject,
 }
 
-/// Append values from a `Float64Array` into `output`, applying the given null 
policy.
+/// Append values from a primitive array into `output`, applying the given 
null policy.
 ///
 /// When there are no nulls the fast path copies the underlying buffer 
directly.
-pub fn handle_float64_nulls(
-    output: &mut Vec<f64>,
-    float_array: &Float64Array,
+pub(crate) fn handle_primitive_nulls<P: ArrowPrimitiveType>(
+    output: &mut Vec<P::Native>,
+    array: &PrimitiveArray<P>,
     null_handling: NullHandling,
-) -> crate::error::Result<()> {
-    if float_array.null_count() == 0 {
-        output.extend_from_slice(float_array.values());
+) -> Result<()>
+where
+    P::Native: Default,
+{
+    if array.null_count() == 0 {
+        output.extend_from_slice(array.values());
     } else {
         match null_handling {
             NullHandling::FillZero => {
-                output.extend(float_array.iter().map(|opt| 
opt.unwrap_or(0.0)));
+                output.extend(array.iter().map(|opt| opt.unwrap_or_default()));
             }
             NullHandling::Reject => {
-                return Err(crate::error::MahoutError::InvalidInput(
-                    "Null value encountered in Float64Array. \
+                return Err(MahoutError::InvalidInput(format!(
+                    "Null value encountered in {:?} array. \
                      Use NullHandling::FillZero to replace nulls with 0.0, \
-                     or clean the data at the source."
-                        .to_string(),
-                ));
+                     or clean the data at the source.",
+                    P::DATA_TYPE,
+                )));
             }
         }
     }
     Ok(())
 }
 
+/// Append values from a `Float64Array` into `output`, applying the given null 
policy.
+///
+/// When there are no nulls the fast path copies the underlying buffer 
directly.
+pub fn handle_float64_nulls(
+    output: &mut Vec<f64>,
+    float_array: &Float64Array,
+    null_handling: NullHandling,
+) -> Result<()> {
+    handle_primitive_nulls::<Float64Type>(output, float_array, null_handling)
+}
+
+/// Append values from a `Float32Array` into `output`, applying the given null 
policy.
+///
+/// When there are no nulls the fast path copies the underlying buffer 
directly.
+pub fn handle_float32_nulls(
+    output: &mut Vec<f32>,
+    float_array: &Float32Array,
+    null_handling: NullHandling,
+) -> Result<()> {
+    handle_primitive_nulls::<Float32Type>(output, float_array, null_handling)
+}
+
 /// Generic data reader interface for batch quantum data.
 ///
 /// Implementations should read data in the format:
diff --git a/qdp/qdp-core/src/readers/parquet.rs 
b/qdp/qdp-core/src/readers/parquet.rs
index 0c9a3e162..30f2deead 100644
--- a/qdp/qdp-core/src/readers/parquet.rs
+++ b/qdp/qdp-core/src/readers/parquet.rs
@@ -17,24 +17,170 @@
 //! Parquet format reader implementation.
 
 use std::fs::File;
+use std::marker::PhantomData;
 use std::path::Path;
 
-use arrow::array::{Array, FixedSizeListArray, Float64Array, ListArray};
-use arrow::datatypes::DataType;
+use arrow::array::{Array, ArrayRef, FixedSizeListArray, ListArray, 
PrimitiveArray};
+use arrow::compute;
+use arrow::datatypes::{ArrowPrimitiveType, DataType};
 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
 
 use crate::error::{MahoutError, Result};
-use crate::reader::{DataReader, NullHandling, StreamingDataReader, 
handle_float64_nulls};
+use crate::reader::{
+    ArrowPrimitive, DataReader, FloatElem, NullHandling, StreamingDataReader,
+    handle_primitive_nulls,
+};
 
-/// Reader for Parquet files containing List<Float64> or 
FixedSizeList<Float64> columns.
-pub struct ParquetReader {
+// ---------------------------------------------------------------------------
+// Module-level helpers
+// ---------------------------------------------------------------------------
+
+fn is_supported_float(dt: &DataType) -> bool {
+    matches!(dt, DataType::Float32 | DataType::Float64)
+}
+
+fn validate_float_list_schema(field: &arrow::datatypes::Field) -> Result<()> {
+    match field.data_type() {
+        DataType::List(child_field) => {
+            if !is_supported_float(child_field.data_type()) {
+                return Err(MahoutError::InvalidInput(format!(
+                    "Expected List<Float32> or List<Float64> column, got 
List<{:?}>",
+                    child_field.data_type()
+                )));
+            }
+        }
+        DataType::FixedSizeList(child_field, _) => {
+            if !is_supported_float(child_field.data_type()) {
+                return Err(MahoutError::InvalidInput(format!(
+                    "Expected FixedSizeList<Float32> or FixedSizeList<Float64> 
column, \
+                     got FixedSizeList<{:?}>",
+                    child_field.data_type()
+                )));
+            }
+        }
+        _ => {
+            return Err(MahoutError::InvalidInput(format!(
+                "Expected List<Float32/Float64> or 
FixedSizeList<Float32/Float64> column, \
+                 got {:?}",
+                field.data_type()
+            )));
+        }
+    }
+    Ok(())
+}
+
+fn validate_float_list_or_scalar_schema(field: &arrow::datatypes::Field) -> 
Result<()> {
+    match field.data_type() {
+        DataType::Float32 | DataType::Float64 => Ok(()),
+        _ => validate_float_list_schema(field),
+    }
+}
+
+/// Returns the element DataType from a List, FixedSizeList, or scalar float 
field.
+fn element_dtype(field: &arrow::datatypes::Field) -> Option<DataType> {
+    match field.data_type() {
+        DataType::List(child) | DataType::FixedSizeList(child, _) => {
+            Some(child.data_type().clone())
+        }
+        dt if is_supported_float(dt) => Some(dt.clone()),
+        _ => None,
+    }
+}
+
+/// Extracts the offset-adjusted flat values slice from a `ListArray`.
+///
+/// `ListArray::values()` returns the full backing child array; for a sliced
+/// `ListArray` the first valid element starts at `offsets[0]`, not index 0.
+/// Omitting this adjustment would read stale data outside the array's range.
+fn list_flat_values(arr: &ListArray) -> ArrayRef {
+    let offsets = arr.offsets();
+    let start = offsets[0] as usize;
+    let end = offsets[arr.len()] as usize;
+    arr.values().slice(start, end - start)
+}
+
+/// Extracts the offset-adjusted flat values slice from a `FixedSizeListArray`.
+///
+/// `FixedSizeListArray::values()` returns the full backing child array; for a 
sliced
+/// array the valid range starts at `offset * value_size`, not at index 0.
+fn fixed_size_list_flat_values(arr: &FixedSizeListArray) -> ArrayRef {
+    let size = arr.value_length() as usize;
+    let start = arr.offset() * size;
+    let end = (arr.offset() + arr.len()) * size;
+    arr.values().slice(start, end - start)
+}
+
+/// Cast `array` to `P::DATA_TYPE` if needed, then append all values to 
`output`.
+///
+/// Same dtype → zero-copy extend from the Arrow buffer.
+/// Cross dtype (f64→f32) → `arrow::compute::cast` once, then extend.
+///   - f64→f32: values outside f32 range become ±Inf; NaN preserved.
+fn extend_floats<P: ArrowPrimitiveType>(
+    output: &mut Vec<P::Native>,
+    array: &dyn Array,
+    null_handling: NullHandling,
+) -> Result<()>
+where
+    P::Native: Default,
+{
+    let target_dt = P::DATA_TYPE;
+    let casted;
+    let effective: &dyn Array = if array.data_type() == &target_dt {
+        array
+    } else if is_supported_float(array.data_type()) {
+        casted = compute::cast(array, &target_dt).map_err(|e| {
+            MahoutError::InvalidInput(format!(
+                "Arrow cast {:?}→{:?}: {e}",
+                array.data_type(),
+                target_dt
+            ))
+        })?;
+        &*casted
+    } else {
+        return Err(MahoutError::InvalidInput(format!(
+            "Expected Float32 or Float64 values, got {:?}",
+            array.data_type()
+        )));
+    };
+
+    let arr = effective
+        .as_any()
+        .downcast_ref::<PrimitiveArray<P>>()
+        .ok_or_else(|| MahoutError::InvalidInput(format!("{:?} downcast 
failed", target_dt)))?;
+    handle_primitive_nulls::<P>(output, arr, null_handling)
+}
+
+fn collect_floats<P: ArrowPrimitiveType>(
+    array: &dyn Array,
+    null_handling: NullHandling,
+) -> Result<Vec<P::Native>>
+where
+    P::Native: Default,
+{
+    let mut out = Vec::new();
+    extend_floats::<P>(&mut out, array, null_handling)?;
+    Ok(out)
+}
+
+// ---------------------------------------------------------------------------
+// ParquetReader<T>
+// ---------------------------------------------------------------------------
+
+/// Reader for Parquet files containing `List<Float32/Float64>` or
+/// `FixedSizeList<Float32/Float64>` columns.
+///
+/// Generic over `T` (`f32` or `f64`):
+/// - same dtype as the file → zero-copy path via `extend_from_slice`
+/// - different dtype → `arrow::compute::cast` (f64→f32: overflow → ±Inf; NaN 
preserved)
+pub struct ParquetReader<T: FloatElem = f64> {
     reader: Option<parquet::arrow::arrow_reader::ParquetRecordBatchReader>,
     sample_size: Option<usize>,
     total_rows: usize,
     null_handling: NullHandling,
+    _phantom: PhantomData<T>,
 }
 
-impl ParquetReader {
+impl<T: FloatElem> ParquetReader<T> {
     /// Create a new Parquet reader.
     ///
     /// # Arguments
@@ -48,7 +194,6 @@ impl ParquetReader {
     ) -> Result<Self> {
         let path = path.as_ref();
 
-        // Verify file exists
         match path.try_exists() {
             Ok(false) => {
                 return Err(MahoutError::Io(format!(
@@ -85,29 +230,16 @@ impl ParquetReader {
             )));
         }
 
-        let field = &schema.fields()[0];
-        match field.data_type() {
-            DataType::List(child_field) => {
-                if !matches!(child_field.data_type(), DataType::Float64) {
-                    return Err(MahoutError::InvalidInput(format!(
-                        "Expected List<Float64> column, got List<{:?}>",
-                        child_field.data_type()
-                    )));
-                }
-            }
-            DataType::FixedSizeList(child_field, _) => {
-                if !matches!(child_field.data_type(), DataType::Float64) {
-                    return Err(MahoutError::InvalidInput(format!(
-                        "Expected FixedSizeList<Float64> column, got 
FixedSizeList<{:?}>",
-                        child_field.data_type()
-                    )));
-                }
-            }
-            _ => {
-                return Err(MahoutError::InvalidInput(format!(
-                    "Expected List<Float64> or FixedSizeList<Float64> column, 
got {:?}",
-                    field.data_type()
-                )));
+        validate_float_list_schema(&schema.fields()[0])?;
+
+        // Warn on f64→f32 narrowing cast: overflow becomes ±Inf with no error.
+        if let Some(file_dt) = element_dtype(&schema.fields()[0]) {
+            let target_dt = <<T as ArrowPrimitive>::ArrowType as 
ArrowPrimitiveType>::DATA_TYPE;
+            if file_dt == DataType::Float64 && target_dt == DataType::Float32 {
+                log::warn!(
+                    "Parquet column is Float64 but reading as f32: values 
outside f32 range \
+                     become ±Inf. Use ParquetReader::<f64> to preserve 
precision."
+                );
             }
         }
 
@@ -125,20 +257,21 @@ impl ParquetReader {
             sample_size: None,
             total_rows,
             null_handling,
+            _phantom: PhantomData,
         })
     }
 }
 
-impl DataReader for ParquetReader {
-    fn read_batch(&mut self) -> Result<(Vec<f64>, usize, usize)> {
+impl<T: FloatElem> DataReader<T> for ParquetReader<T> {
+    fn read_batch(&mut self) -> Result<(Vec<T>, usize, usize)> {
         let reader = self
             .reader
             .take()
             .ok_or_else(|| MahoutError::InvalidInput("Reader already 
consumed".to_string()))?;
 
-        let mut all_data = Vec::new();
+        let mut all_data: Vec<T> = Vec::new();
         let mut num_samples = 0;
-        let mut sample_size = None;
+        let mut sample_size: Option<usize> = None;
 
         for batch_result in reader {
             let batch = batch_result
@@ -157,33 +290,31 @@ impl DataReader for ParquetReader {
                             MahoutError::Io("Failed to downcast to 
ListArray".to_string())
                         })?;
 
+                    // Validate all rows have a consistent sample size.
                     for i in 0..list_array.len() {
-                        let value_array = list_array.value(i);
-                        let float_array = value_array
-                            .as_any()
-                            .downcast_ref::<Float64Array>()
-                            .ok_or_else(|| {
-                                MahoutError::Io("List values must be 
Float64".to_string())
-                            })?;
-
-                        let current_size = float_array.len();
-
-                        if let Some(expected_size) = sample_size {
-                            if current_size != expected_size {
+                        let row_len = list_array.value_length(i) as usize;
+                        if let Some(expected) = sample_size {
+                            if row_len != expected {
                                 return Err(MahoutError::InvalidInput(format!(
                                     "Inconsistent sample sizes: expected {}, 
got {}",
-                                    expected_size, current_size
+                                    expected, row_len
                                 )));
                             }
                         } else {
-                            sample_size = Some(current_size);
-                            all_data.reserve(current_size * self.total_rows);
+                            sample_size = Some(row_len);
+                            all_data.reserve(row_len * self.total_rows);
                         }
-
-                        handle_float64_nulls(&mut all_data, float_array, 
self.null_handling)?;
-
-                        num_samples += 1;
                     }
+
+                    // Cast the entire flat buffer once (avoids N per-row 
allocations
+                    // on cross-dtype reads) then extend all_data in one pass.
+                    let flat = list_flat_values(list_array);
+                    extend_floats::<<T as ArrowPrimitive>::ArrowType>(
+                        &mut all_data,
+                        &*flat,
+                        self.null_handling,
+                    )?;
+                    num_samples += list_array.len();
                 }
                 DataType::FixedSizeList(_, size) => {
                     let list_array = column
@@ -200,19 +331,18 @@ impl DataReader for ParquetReader {
                         all_data.reserve(current_size * batch.num_rows());
                     }
 
-                    let values = list_array.values();
-                    let float_array = values
-                        .as_any()
-                        .downcast_ref::<Float64Array>()
-                        .ok_or_else(|| MahoutError::Io("Values must be 
Float64".to_string()))?;
-
-                    handle_float64_nulls(&mut all_data, float_array, 
self.null_handling)?;
-
+                    let flat = fixed_size_list_flat_values(list_array);
+                    extend_floats::<<T as ArrowPrimitive>::ArrowType>(
+                        &mut all_data,
+                        &*flat,
+                        self.null_handling,
+                    )?;
                     num_samples += list_array.len();
                 }
                 _ => {
-                    return Err(MahoutError::Io(format!(
-                        "Expected List<Float64> or FixedSizeList<Float64>, got 
{:?}",
+                    return Err(MahoutError::InvalidInput(format!(
+                        "Expected List<Float32/Float64> or 
FixedSizeList<Float32/Float64>, \
+                         got {:?}",
                         column.data_type()
                     )));
                 }
@@ -236,20 +366,26 @@ impl DataReader for ParquetReader {
     }
 }
 
-/// Streaming Parquet reader for List<Float64> and FixedSizeList<Float64> 
columns.
+// ---------------------------------------------------------------------------
+// ParquetStreamingReader<T>
+// ---------------------------------------------------------------------------
+
+/// Streaming Parquet reader for `List<Float32/Float64>` and
+/// `FixedSizeList<Float32/Float64>` columns.
 ///
-/// Reads Parquet files in chunks without loading entire file into memory.
-/// Supports efficient streaming for large files via Producer-Consumer pattern.
-pub struct ParquetStreamingReader {
+/// Reads Parquet files in chunks without loading the entire file into memory.
+/// Supports efficient streaming for large files via the Producer-Consumer 
pattern.
+pub struct ParquetStreamingReader<T: FloatElem = f64> {
     reader: parquet::arrow::arrow_reader::ParquetRecordBatchReader,
     sample_size: Option<usize>,
-    leftover_data: Vec<f64>,
+    leftover_data: Vec<T>,
     leftover_cursor: usize,
     pub total_rows: usize,
     null_handling: NullHandling,
+    _phantom: PhantomData<T>,
 }
 
-impl ParquetStreamingReader {
+impl<T: FloatElem> ParquetStreamingReader<T> {
     /// Create a new streaming Parquet reader.
     ///
     /// # Arguments
@@ -263,7 +399,6 @@ impl ParquetStreamingReader {
     ) -> Result<Self> {
         let path = path.as_ref();
 
-        // Verify file exists
         match path.try_exists() {
             Ok(false) => {
                 return Err(MahoutError::Io(format!(
@@ -300,32 +435,17 @@ impl ParquetStreamingReader {
             )));
         }
 
-        let field = &schema.fields()[0];
-        match field.data_type() {
-            DataType::List(child_field) => {
-                if !matches!(child_field.data_type(), DataType::Float64) {
-                    return Err(MahoutError::InvalidInput(format!(
-                        "Expected List<Float64> column, got List<{:?}>",
-                        child_field.data_type()
-                    )));
-                }
-            }
-            DataType::FixedSizeList(child_field, _) => {
-                if !matches!(child_field.data_type(), DataType::Float64) {
-                    return Err(MahoutError::InvalidInput(format!(
-                        "Expected FixedSizeList<Float64> column, got 
FixedSizeList<{:?}>",
-                        child_field.data_type()
-                    )));
-                }
-            }
-            DataType::Float64 => {
-                // Scalar Float64 for basis encoding (one index per sample)
-            }
-            _ => {
-                return Err(MahoutError::InvalidInput(format!(
-                    "Expected Float64, List<Float64>, or 
FixedSizeList<Float64> column, got {:?}",
-                    field.data_type()
-                )));
+        validate_float_list_or_scalar_schema(&schema.fields()[0])?;
+
+        // Warn on f64→f32 narrowing cast: overflow becomes ±Inf with no error.
+        if let Some(file_dt) = element_dtype(&schema.fields()[0]) {
+            let target_dt = <<T as ArrowPrimitive>::ArrowType as 
ArrowPrimitiveType>::DATA_TYPE;
+            if file_dt == DataType::Float64 && target_dt == DataType::Float32 {
+                log::warn!(
+                    "ParquetStreamingReader: Float64 column cast to f32 — 
values outside \
+                     f32 range become ±Inf. Use ParquetStreamingReader::<f64> 
to preserve \
+                     precision."
+                );
             }
         }
 
@@ -344,6 +464,7 @@ impl ParquetStreamingReader {
             leftover_cursor: 0,
             total_rows,
             null_handling,
+            _phantom: PhantomData,
         })
     }
 
@@ -353,13 +474,14 @@ impl ParquetStreamingReader {
     }
 }
 
-impl DataReader for ParquetStreamingReader {
-    fn read_batch(&mut self) -> Result<(Vec<f64>, usize, usize)> {
+impl<T: FloatElem> DataReader<T> for ParquetStreamingReader<T> {
+    fn read_batch(&mut self) -> Result<(Vec<T>, usize, usize)> {
         let mut all_data = Vec::new();
         let mut num_samples = 0;
 
+        // Hoist buffer out of the loop to avoid re-allocating 1M elements per 
iteration.
+        let mut buffer = vec![T::default(); 1024 * 1024];
         loop {
-            let mut buffer = vec![0.0; 1024 * 1024]; // 1M elements buffer
             let written = self.read_chunk(&mut buffer)?;
             if written == 0 {
                 break;
@@ -384,8 +506,8 @@ impl DataReader for ParquetStreamingReader {
     }
 }
 
-impl StreamingDataReader for ParquetStreamingReader {
-    fn read_chunk(&mut self, buffer: &mut [f64]) -> Result<usize> {
+impl<T: FloatElem> StreamingDataReader<T> for ParquetStreamingReader<T> {
+    fn read_chunk(&mut self, buffer: &mut [T]) -> Result<usize> {
         let mut written = 0;
         let buf_cap = buffer.len();
         let calc_limit = |ss: usize| -> usize {
@@ -435,37 +557,32 @@ impl StreamingDataReader for ParquetStreamingReader {
                                     MahoutError::Io("Failed to downcast to 
ListArray".to_string())
                                 })?;
 
-                            if list_array.len() == 0 {
+                            if list_array.is_empty() {
                                 continue;
                             }
 
-                            let mut batch_values = Vec::new();
-                            let mut current_sample_size = None;
-                            for i in 0..list_array.len() {
-                                let value_array = list_array.value(i);
-                                let float_array = value_array
-                                    .as_any()
-                                    .downcast_ref::<Float64Array>()
-                                    .ok_or_else(|| {
-                                        MahoutError::Io("List values must be 
Float64".to_string())
-                                    })?;
-
-                                if i == 0 {
-                                    current_sample_size = 
Some(float_array.len());
-                                }
+                            let current_sample_size = 
list_array.value_length(0) as usize;
 
-                                handle_float64_nulls(
-                                    &mut batch_values,
-                                    float_array,
-                                    self.null_handling,
-                                )?;
+                            // Validate all rows in this batch have a 
consistent sample size.
+                            for i in 1..list_array.len() {
+                                let row_len = list_array.value_length(i) as 
usize;
+                                if row_len != current_sample_size {
+                                    return 
Err(MahoutError::InvalidInput(format!(
+                                        "Inconsistent sample sizes: expected 
{}, got {}",
+                                        current_sample_size, row_len
+                                    )));
+                                }
                             }
 
-                            (
-                                current_sample_size
-                                    .expect("list_array.len() > 0 ensures at 
least one element"),
-                                batch_values,
-                            )
+                            // Cast the entire flat buffer once (avoids N 
per-row allocations
+                            // on cross-dtype reads).
+                            let flat = list_flat_values(list_array);
+                            let batch_values = collect_floats::<<T as 
ArrowPrimitive>::ArrowType>(
+                                &*flat,
+                                self.null_handling,
+                            )?;
+
+                            (current_sample_size, batch_values)
                         }
                         DataType::FixedSizeList(_, size) => {
                             let list_array = column
@@ -477,60 +594,35 @@ impl StreamingDataReader for ParquetStreamingReader {
                                 )
                             })?;
 
-                            if list_array.len() == 0 {
+                            if list_array.is_empty() {
                                 continue;
                             }
 
                             let current_sample_size = *size as usize;
-
-                            let values = list_array.values();
-                            let float_array = values
-                                .as_any()
-                                .downcast_ref::<Float64Array>()
-                                .ok_or_else(|| {
-                                    MahoutError::Io(
-                                        "FixedSizeList values must be 
Float64".to_string(),
-                                    )
-                                })?;
-
-                            let mut batch_values = Vec::new();
-                            handle_float64_nulls(
-                                &mut batch_values,
-                                float_array,
+                            let flat = fixed_size_list_flat_values(list_array);
+                            let batch_values = collect_floats::<<T as 
ArrowPrimitive>::ArrowType>(
+                                &*flat,
                                 self.null_handling,
                             )?;
 
                             (current_sample_size, batch_values)
                         }
-                        DataType::Float64 => {
-                            // Scalar Float64 for basis encoding (one index 
per sample)
-                            let float_array = column
-                                .as_any()
-                                .downcast_ref::<Float64Array>()
-                                .ok_or_else(|| {
-                                    MahoutError::Io(
-                                        "Failed to downcast to 
Float64Array".to_string(),
-                                    )
-                                })?;
-
-                            if float_array.is_empty() {
+                        DataType::Float32 | DataType::Float64 => {
+                            // Scalar float for basis encoding (one index per 
sample)
+                            if column.is_empty() {
                                 continue;
                             }
-
                             let current_sample_size = 1;
-
-                            let mut batch_values = Vec::new();
-                            handle_float64_nulls(
-                                &mut batch_values,
-                                float_array,
+                            let batch_values = collect_floats::<<T as 
ArrowPrimitive>::ArrowType>(
+                                &**column,
                                 self.null_handling,
                             )?;
-
                             (current_sample_size, batch_values)
                         }
                         _ => {
-                            return Err(MahoutError::Io(format!(
-                                "Expected Float64, List<Float64>, or 
FixedSizeList<Float64>, got {:?}",
+                            return Err(MahoutError::InvalidInput(format!(
+                                "Expected Float32/Float64, 
List<Float32/Float64>, or \
+                                 FixedSizeList<Float32/Float64>, got {:?}",
                                 column.data_type()
                             )));
                         }
@@ -581,6 +673,11 @@ impl StreamingDataReader for ParquetStreamingReader {
         self.total_rows
     }
 }
+
+// ---------------------------------------------------------------------------
+// Unit tests
+// ---------------------------------------------------------------------------
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -634,7 +731,7 @@ mod tests {
     #[test]
     fn test_parquet_reader_missing_file() {
         let path = std::env::temp_dir().join(format!("missing_{}.parquet", 
std::process::id()));
-        let result = ParquetReader::new(&path, None, NullHandling::FillZero);
+        let result = ParquetReader::<f64>::new(&path, None, 
NullHandling::FillZero);
         assert!(matches!(result, Err(MahoutError::Io(_))));
     }
 
@@ -644,13 +741,17 @@ mod tests {
         let array = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
         let file = write_test_parquet(schema, vec![array]);
 
-        let result = ParquetReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
             Ok(_) => panic!(),
         };
-        assert!(err_msg.contains("Expected List<Float64> or 
FixedSizeList<Float64> column"));
+        // The error must mention the actual dtype, not just a generic 
"Expected" substring.
+        assert!(
+            err_msg.contains("Int32"),
+            "error message should contain the column dtype, got: {err_msg}"
+        );
     }
 
     #[test]
@@ -663,7 +764,7 @@ mod tests {
         let arr2 = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
         let file = write_test_parquet(schema, vec![arr1, arr2]);
 
-        let result = ParquetReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
@@ -685,13 +786,13 @@ mod tests {
 
         let file = write_test_parquet(schema, vec![array]);
 
-        let result = ParquetReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
             Ok(_) => panic!(),
         };
-        assert!(err_msg.contains("Expected List<Float64> column"));
+        assert!(err_msg.contains("Expected List<Float32> or List<Float64>"));
     }
 
     #[test]
@@ -709,7 +810,8 @@ mod tests {
 
         let file = write_test_parquet(schema, vec![array]);
 
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let (data, num_samples, sample_size) = reader.read_batch().unwrap();
         assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0]);
         assert_eq!(num_samples, 2);
@@ -733,7 +835,8 @@ mod tests {
 
         let file = write_test_parquet(schema, vec![array]);
 
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let (data, num_samples, sample_size) = reader.read_batch().unwrap();
         assert_eq!(data, vec![5.0, 6.0, 7.0, 8.0]);
         assert_eq!(num_samples, 2);
@@ -755,7 +858,8 @@ mod tests {
 
         let file = write_test_parquet(schema, vec![array]);
 
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let result = reader.read_batch();
         assert!(result.is_err());
         let err_msg = match result {
@@ -767,6 +871,7 @@ mod tests {
 
     #[test]
     fn test_parquet_streaming_reader_scalar_f64() {
+        use arrow::array::Float64Builder;
         let schema = Arc::new(Schema::new(vec![Field::new(
             "data",
             DataType::Float64,
@@ -778,7 +883,7 @@ mod tests {
         let file = write_test_parquet(schema, vec![array]);
 
         let mut streaming_reader =
-            ParquetStreamingReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+            ParquetStreamingReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         assert_eq!(streaming_reader.total_rows(), 3);
         let mut buffer = vec![0.0; 2];
         let written1 = streaming_reader.read_chunk(&mut buffer).unwrap();
@@ -812,7 +917,8 @@ mod tests {
         let file = write_test_parquet(schema, vec![array]);
 
         let mut reader =
-            ParquetStreamingReader::new(file.path(), Some(1), 
NullHandling::FillZero).unwrap();
+            ParquetStreamingReader::<f64>::new(file.path(), Some(1), 
NullHandling::FillZero)
+                .unwrap();
         let (data, num_samples, sample_size) = reader.read_batch().unwrap();
         assert_eq!(data, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
         assert_eq!(num_samples, 3);
@@ -829,7 +935,8 @@ mod tests {
         let array = Arc::new(builder.finish()) as ArrayRef;
         let file = write_test_parquet(schema, vec![array]);
 
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let result = reader.read_batch();
         assert!(result.is_err());
         let err_msg = match result {
@@ -850,7 +957,7 @@ mod tests {
         let file = write_test_parquet(schema, vec![array]);
 
         let mut reader =
-            ParquetStreamingReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+            ParquetStreamingReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let result = reader.read_batch();
         assert!(result.is_err());
         let err_msg = match result {
@@ -899,7 +1006,8 @@ mod tests {
     #[test]
     fn test_parquet_reader_list_f64_null_fill_zero() {
         let file = write_list_parquet_with_nulls();
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let (data, num_samples, sample_size) = reader.read_batch().unwrap();
         assert_eq!(data, vec![1.0, 0.0, 3.0, 4.0]);
         assert_eq!(num_samples, 2);
@@ -909,7 +1017,8 @@ mod tests {
     #[test]
     fn test_parquet_reader_list_f64_null_reject() {
         let file = write_list_parquet_with_nulls();
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::Reject).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::Reject).unwrap();
         let result = reader.read_batch();
         assert!(result.is_err());
         let err_msg = match result {
@@ -922,7 +1031,8 @@ mod tests {
     #[test]
     fn test_parquet_reader_fixed_size_list_null_fill_zero() {
         let file = write_fixed_size_list_parquet_with_nulls();
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::FillZero).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::FillZero).unwrap();
         let (data, num_samples, sample_size) = reader.read_batch().unwrap();
         assert_eq!(data, vec![1.0, 0.0, 3.0, 4.0]);
         assert_eq!(num_samples, 2);
@@ -932,7 +1042,8 @@ mod tests {
     #[test]
     fn test_parquet_reader_fixed_size_list_null_reject() {
         let file = write_fixed_size_list_parquet_with_nulls();
-        let mut reader = ParquetReader::new(file.path(), None, 
NullHandling::Reject).unwrap();
+        let mut reader =
+            ParquetReader::<f64>::new(file.path(), None, 
NullHandling::Reject).unwrap();
         let result = reader.read_batch();
         assert!(result.is_err());
         let err_msg = match result {
@@ -949,7 +1060,7 @@ mod tests {
         let file = TempTestFile::new();
         let path = file.path().to_path_buf();
         drop(file);
-        let result = ParquetStreamingReader::new(&path, None, 
NullHandling::FillZero);
+        let result = ParquetStreamingReader::<f64>::new(&path, None, 
NullHandling::FillZero);
         assert!(matches!(result, Err(MahoutError::Io(_))));
     }
 
@@ -959,7 +1070,7 @@ mod tests {
         let array = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
         let file = write_test_parquet(schema, vec![array]);
 
-        let result = ParquetStreamingReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetStreamingReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
@@ -978,7 +1089,7 @@ mod tests {
         let arr2 = Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef;
         let file = write_test_parquet(schema, vec![arr1, arr2]);
 
-        let result = ParquetStreamingReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetStreamingReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
@@ -1000,12 +1111,12 @@ mod tests {
 
         let file = write_test_parquet(schema, vec![array]);
 
-        let result = ParquetStreamingReader::new(file.path(), None, 
NullHandling::FillZero);
+        let result = ParquetStreamingReader::<f64>::new(file.path(), None, 
NullHandling::FillZero);
         assert!(result.is_err());
         let err_msg = match result {
             Err(e) => e.to_string(),
             Ok(_) => panic!(),
         };
-        assert!(err_msg.contains("Expected List<Float64>"));
+        assert!(err_msg.contains("Expected List<Float32> or List<Float64>"));
     }
 }
diff --git a/qdp/qdp-core/src/remote.rs b/qdp/qdp-core/src/remote.rs
index a2577eb6c..c800fe52f 100644
--- a/qdp/qdp-core/src/remote.rs
+++ b/qdp/qdp-core/src/remote.rs
@@ -256,7 +256,7 @@ mod tests {
 
         // Verify it's a valid parquet that our reader can parse.
         use crate::reader::DataReader;
-        let mut reader = crate::readers::ParquetReader::new(
+        let mut reader = crate::readers::ParquetReader::<f64>::new(
             &resolved.path,
             None,
             crate::reader::NullHandling::FillZero,
diff --git a/qdp/qdp-core/tests/parquet_f32.rs 
b/qdp/qdp-core/tests/parquet_f32.rs
new file mode 100644
index 000000000..ecfd293db
--- /dev/null
+++ b/qdp/qdp-core/tests/parquet_f32.rs
@@ -0,0 +1,297 @@
+//
+// 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.
+
+//! Acceptance tests for ParquetReader<f32> — issue #1340.
+
+use std::fs;
+use std::sync::Arc;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use arrow::array::{
+    ArrayRef, FixedSizeListBuilder, Float32Builder, Float64Builder, 
ListBuilder, RecordBatch,
+};
+use arrow::datatypes::{DataType, Field, Schema};
+use parquet::arrow::ArrowWriter;
+use qdp_core::reader::{DataReader, NullHandling};
+use qdp_core::readers::parquet::{ParquetReader, ParquetStreamingReader};
+
+static FILE_COUNTER: AtomicUsize = AtomicUsize::new(0);
+
+struct TempFile(std::path::PathBuf);
+
+impl TempFile {
+    fn path(&self) -> &std::path::Path {
+        &self.0
+    }
+}
+
+impl Drop for TempFile {
+    fn drop(&mut self) {
+        let _ = fs::remove_file(&self.0);
+    }
+}
+
+fn write_list_parquet(schema: Arc<Schema>, arrays: Vec<ArrayRef>) -> TempFile {
+    let n = FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
+    let path = std::env::temp_dir().join(format!(
+        "mahout_parquet_f32_{}_{}.parquet",
+        std::process::id(),
+        n,
+    ));
+    let batch = RecordBatch::try_new(schema.clone(), arrays).unwrap();
+    let file = fs::File::create(&path).unwrap();
+    let mut writer = ArrowWriter::try_new(file, schema, None).unwrap();
+    writer.write(&batch).unwrap();
+    writer.close().unwrap();
+    TempFile(path)
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 1: f32 column read as f32 (zero-copy path)
+// ---------------------------------------------------------------------------
+
+/// ParquetReader::<f32> on a List<Float32> file → values come back as 
Vec<f32>,
+/// no precision loss, correct count.
+#[test]
+fn test_f32_column_read_as_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float32, true));
+    let list_field = Field::new("data", DataType::List(item_field), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = ListBuilder::new(Float32Builder::new());
+    builder.values().append_slice(&[1.0_f32, 2.5_f32, 3.75_f32]);
+    builder.append(true);
+    builder.values().append_slice(&[4.0_f32, 5.5_f32, 6.25_f32]);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader = ParquetReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 3);
+    assert_eq!(data, vec![1.0_f32, 2.5, 3.75, 4.0, 5.5, 6.25]);
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 2: f64 column cast to f32
+// ---------------------------------------------------------------------------
+
+/// ParquetReader::<f32> on a List<Float64> file → Arrow cast applied.
+/// Normal values: cast is precise within f32 range.
+/// Overflow (f64 > f32::MAX): → +Inf (Arrow safe cast behaviour).
+/// NaN: preserved.
+#[test]
+fn test_f64_column_cast_to_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float64, true));
+    let list_field = Field::new("data", DataType::List(item_field), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let overflow = f64::from(f32::MAX) * 2.0; // overflows f32 → +Inf after 
cast
+    let nan = f64::NAN;
+
+    let mut builder = ListBuilder::new(Float64Builder::new());
+    builder.values().append_slice(&[1.0_f64, -2.0_f64]);
+    builder.append(true);
+    builder.values().append_value(overflow);
+    builder.values().append_value(nan);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader = ParquetReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 2);
+    assert_eq!(data[0], 1.0_f32);
+    assert_eq!(data[1], -2.0_f32);
+    assert!(
+        data[2].is_infinite() && data[2] > 0.0,
+        "expected +Inf, got {}",
+        data[2]
+    );
+    assert!(data[3].is_nan(), "expected NaN, got {}", data[3]);
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 3: unsupported column type → InvalidInput with dtype in 
message
+// ---------------------------------------------------------------------------
+
+/// ParquetReader on a List<Int32> file must fail at construction with an
+/// InvalidInput error whose message mentions the actual column dtype.
+#[test]
+fn test_unsupported_column_type_returns_error_with_dtype() {
+    let item_field = Arc::new(Field::new("item", DataType::Int32, true));
+    let list_field = Field::new("data", DataType::List(item_field), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = 
arrow::array::ListBuilder::new(arrow::array::Int32Builder::new());
+    builder.values().append_value(42);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let result_f32 = ParquetReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero);
+    let result_f64 = ParquetReader::<f64>::new(tmp.path(), None, 
NullHandling::FillZero);
+
+    for result in [result_f32.map(|_| ()), result_f64.map(|_| ())] {
+        assert!(result.is_err(), "expected error for Int32 column");
+        let msg = result.unwrap_err().to_string();
+        assert!(
+            msg.contains("Int32") || msg.contains("int32"),
+            "error message should contain the dtype, got: {msg}"
+        );
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 4: FixedSizeList<f32> read as f32 (zero-copy path)
+// ---------------------------------------------------------------------------
+
+/// ParquetReader::<f32> on a FixedSizeList<Float32> file → values come back as
+/// Vec<f32> with no precision loss.
+#[test]
+fn test_fixed_size_list_f32_as_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float32, true));
+    let list_field = Field::new("data", DataType::FixedSizeList(item_field, 
3), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = FixedSizeListBuilder::new(Float32Builder::new(), 3);
+    builder.values().append_slice(&[1.0_f32, 2.0_f32, 3.0_f32]);
+    builder.append(true);
+    builder.values().append_slice(&[4.0_f32, 5.0_f32, 6.0_f32]);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader = ParquetReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 3);
+    assert_eq!(data, vec![1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 5: FixedSizeList<f64> cast to f32
+// ---------------------------------------------------------------------------
+
+/// ParquetReader::<f32> on a FixedSizeList<Float64> file → Arrow cast applied.
+#[test]
+fn test_fixed_size_list_f64_cast_to_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float64, true));
+    let list_field = Field::new("data", DataType::FixedSizeList(item_field, 
2), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = FixedSizeListBuilder::new(Float64Builder::new(), 2);
+    builder.values().append_slice(&[1.5_f64, -2.5_f64]);
+    builder.append(true);
+    builder
+        .values()
+        .append_slice(&[f64::from(f32::MAX) * 2.0, f64::NAN]);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader = ParquetReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 2);
+    assert_eq!(data[0], 1.5_f32);
+    assert_eq!(data[1], -2.5_f32);
+    assert!(
+        data[2].is_infinite() && data[2] > 0.0,
+        "expected +Inf, got {}",
+        data[2]
+    );
+    assert!(data[3].is_nan(), "expected NaN, got {}", data[3]);
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 6: ParquetStreamingReader<f32> on f32 column
+// ---------------------------------------------------------------------------
+
+/// ParquetStreamingReader::<f32> on a List<Float32> file → same values as
+/// ParquetReader::<f32>.
+#[test]
+fn test_streaming_reader_list_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float32, true));
+    let list_field = Field::new("data", DataType::List(item_field), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = ListBuilder::new(Float32Builder::new());
+    builder.values().append_slice(&[1.0_f32, 2.5_f32, 3.75_f32]);
+    builder.append(true);
+    builder.values().append_slice(&[4.0_f32, 5.5_f32, 6.25_f32]);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader =
+        ParquetStreamingReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 3);
+    assert_eq!(data, vec![1.0_f32, 2.5, 3.75, 4.0, 5.5, 6.25]);
+}
+
+// ---------------------------------------------------------------------------
+// Acceptance test 7: ParquetStreamingReader<f32> on f64 column (cast path)
+// ---------------------------------------------------------------------------
+
+/// ParquetStreamingReader::<f32> on a List<Float64> file → Arrow cast applied;
+/// overflow → ±Inf, NaN preserved.
+#[test]
+fn test_streaming_reader_list_f64_cast_to_f32() {
+    let item_field = Arc::new(Field::new("item", DataType::Float64, true));
+    let list_field = Field::new("data", DataType::List(item_field), true);
+    let schema = Arc::new(Schema::new(vec![list_field]));
+
+    let mut builder = ListBuilder::new(Float64Builder::new());
+    builder.values().append_slice(&[1.0_f64, -2.0_f64]);
+    builder.append(true);
+    builder.values().append_value(f64::from(f32::MAX) * 2.0);
+    builder.values().append_value(f64::NAN);
+    builder.append(true);
+    let array = Arc::new(builder.finish()) as ArrayRef;
+
+    let tmp = write_list_parquet(schema, vec![array]);
+
+    let mut reader =
+        ParquetStreamingReader::<f32>::new(tmp.path(), None, 
NullHandling::FillZero).unwrap();
+    let (data, num_samples, sample_size) = reader.read_batch().unwrap();
+
+    assert_eq!(num_samples, 2);
+    assert_eq!(sample_size, 2);
+    assert_eq!(data[0], 1.0_f32);
+    assert_eq!(data[1], -2.0_f32);
+    assert!(
+        data[2].is_infinite() && data[2] > 0.0,
+        "expected +Inf, got {}",
+        data[2]
+    );
+    assert!(data[3].is_nan(), "expected NaN, got {}", data[3]);
+}

Reply via email to