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

andygrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new f22d9f17bb refactor: use arity helper for Int to Decimal128 
reinterpretation (#5193)
f22d9f17bb is described below

commit f22d9f17bb99b5dd60768e8cafd4dd0e312756de
Author: ChenChen Lai <[email protected]>
AuthorDate: Thu Aug 6 02:02:15 2026 +0800

    refactor: use arity helper for Int to Decimal128 reinterpretation (#5193)
---
 native/core/src/execution/columnar_to_row.rs       | 181 +++++++++++------
 native/spark-expr/Cargo.toml                       |   4 +
 native/spark-expr/benches/make_decimal.rs          | 164 +++++++++++++++
 .../src/math_funcs/internal/make_decimal.rs        | 219 ++++++++++++++++++++-
 4 files changed, 503 insertions(+), 65 deletions(-)

diff --git a/native/core/src/execution/columnar_to_row.rs 
b/native/core/src/execution/columnar_to_row.rs
index 14e115cba0..a1321a9def 100644
--- a/native/core/src/execution/columnar_to_row.rs
+++ b/native/core/src/execution/columnar_to_row.rs
@@ -37,10 +37,11 @@
 
 use crate::errors::{CometError, CometResult};
 use arrow::array::types::{
-    ArrowDictionaryKeyType, Int16Type, Int32Type, Int64Type, Int8Type, 
UInt16Type, UInt32Type,
-    UInt64Type, UInt8Type,
+    ArrowDictionaryKeyType, Decimal128Type, Int16Type, Int32Type, Int64Type, 
Int8Type, UInt16Type,
+    UInt32Type, UInt64Type, UInt8Type,
 };
 use arrow::array::*;
+use arrow::compute::kernels::arity::unary;
 use arrow::compute::{cast_with_options, CastOptions};
 use arrow::datatypes::{ArrowNativeType, DataType, TimeUnit};
 use std::sync::Arc;
@@ -1055,14 +1056,10 @@ impl ColumnarToRowContext {
                 // Parquet stores small-precision decimals as Int32 for 
efficiency, and the
                 // reader may surface them as the physical Int32 type. The 
value is already
                 // scaled (e.g., -1 means -0.01 for scale 2). Reinterpret (not 
cast) to
-                // Decimal128 preserving the value.
-                let int_array = 
array.as_any().downcast_ref::<Int32Array>().ok_or_else(|| {
-                    CometError::Internal("Failed to downcast to 
Int32Array".to_string())
-                })?;
-                let decimal_array: Decimal128Array = int_array
-                    .iter()
-                    .map(|v| v.map(|x| x as i128))
-                    .collect::<Decimal128Array>()
+                // Decimal128 preserving the value. `arity::unary` widens the 
value and reuses
+                // the input null buffer zero-copy (an Arrow cast would 
rescale the value).
+                let int_array = array.as_primitive::<Int32Type>();
+                let decimal_array = unary::<_, _, Decimal128Type>(int_array, 
|x| x as i128)
                     .with_precision_and_scale(*precision, *scale)
                     .map_err(|e| {
                         CometError::Internal(format!("Invalid decimal 
precision/scale: {}", e))
@@ -1071,13 +1068,8 @@ impl ColumnarToRowContext {
             }
             (DataType::Int64, DataType::Decimal128(precision, scale)) => {
                 // Same as Int32 but for medium-precision decimals stored as 
Int64.
-                let int_array = 
array.as_any().downcast_ref::<Int64Array>().ok_or_else(|| {
-                    CometError::Internal("Failed to downcast to 
Int64Array".to_string())
-                })?;
-                let decimal_array: Decimal128Array = int_array
-                    .iter()
-                    .map(|v| v.map(|x| x as i128))
-                    .collect::<Decimal128Array>()
+                let int_array = array.as_primitive::<Int64Type>();
+                let decimal_array = unary::<_, _, Decimal128Type>(int_array, 
|x| x as i128)
                     .with_precision_and_scale(*precision, *scale)
                     .map_err(|e| {
                         CometError::Internal(format!("Invalid decimal 
precision/scale: {}", e))
@@ -2580,14 +2572,10 @@ mod tests {
 
     #[test]
     fn test_convert_int32_to_decimal128() {
-        // Test that Int32 arrays are correctly cast to Decimal128 when schema 
expects Decimal128.
-        // This can happen when the parquet reader surfaces small-precision 
decimals as Int32.
-
-        // Create an Int32 array representing decimals: [-1, -2, -3] which at 
scale 2 means
-        // [-0.01, -0.02, -0.03]
-        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![-1i32, -2, 
-3]));
+        // Int32 → Decimal128 reinterpret. Values [-1, null, -3] at scale 2 
mean
+        // [-0.01, null, -0.03]. The null covers the arity::unary null-buffer 
path.
+        let int_array: ArrayRef = Arc::new(Int32Array::from(vec![Some(-1i32), 
None, Some(-3)]));
 
-        // Schema expects Decimal128(5, 2)
         let schema = vec![DataType::Decimal128(5, 2)];
         let mut ctx = ColumnarToRowContext::new(schema, 100);
 
@@ -2598,32 +2586,74 @@ mod tests {
         assert_eq!(offsets.len(), 3);
         assert_eq!(lengths.len(), 3);
 
-        // Verify the decimal values are correct after casting
-        // Fixed-width decimal is stored directly in the 8-byte field slot
+        // Row layout: [8-byte null bitset][8-byte field slot]. Column 0's 
null bit
+        // is bit 0 of the bitset.
         unsafe {
-            for (i, expected) in [-1i64, -2, -3].iter().enumerate() {
-                let row =
-                    std::slice::from_raw_parts(ptr.add(offsets[i] as usize), 
lengths[i] as usize);
-                // Field value starts at offset 8 (after null bitset)
-                let value = i64::from_le_bytes(row[8..16].try_into().unwrap());
-                assert_eq!(
-                    value, *expected,
-                    "Row {} should have value {}, got {}",
-                    i, expected, value
-                );
-            }
+            let row0 =
+                std::slice::from_raw_parts(ptr.add(offsets[0] as usize), 
lengths[0] as usize);
+            let null_bitset_0 = 
i64::from_le_bytes(row0[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_0 & 1, 0, "row 0 should not be null");
+            assert_eq!(i64::from_le_bytes(row0[8..16].try_into().unwrap()), 
-1);
+
+            let row1 =
+                std::slice::from_raw_parts(ptr.add(offsets[1] as usize), 
lengths[1] as usize);
+            let null_bitset_1 = 
i64::from_le_bytes(row1[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_1 & 1, 1, "row 1 should be null");
+
+            let row2 =
+                std::slice::from_raw_parts(ptr.add(offsets[2] as usize), 
lengths[2] as usize);
+            let null_bitset_2 = 
i64::from_le_bytes(row2[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_2 & 1, 0, "row 2 should not be null");
+            assert_eq!(i64::from_le_bytes(row2[8..16].try_into().unwrap()), 
-3);
         }
     }
 
     #[test]
-    fn test_convert_int64_to_decimal128() {
-        // Test that Int64 arrays are correctly cast to Decimal128 when schema 
expects Decimal128.
-        // This can happen when the parquet reader surfaces medium-precision 
decimals as Int64.
+    fn test_convert_int32_to_decimal128_sliced() {
+        // Sliced input must reinterpret correctly. Arrays imported over FFI 
often
+        // carry a non-zero offset; both the value widening and the null buffer
+        // traversal must honor it.
+        let full = Int32Array::from(vec![
+            Some(-99i32), // discarded prefix
+            Some(-98),
+            Some(-1), // start of sliced portion
+            None,
+            Some(-3),
+            Some(-97), // discarded suffix
+        ]);
+        let sliced: ArrayRef = Arc::new(full.slice(2, 3));
+
+        let schema = vec![DataType::Decimal128(5, 2)];
+        let mut ctx = ColumnarToRowContext::new(schema, 100);
+        let arrays = vec![sliced];
+        let (ptr, offsets, lengths) = ctx.convert(&arrays, 3).unwrap();
+
+        assert!(!ptr.is_null());
+        assert_eq!(offsets.len(), 3);
 
-        // Create an Int64 array representing decimals
-        let int_array: ArrayRef = Arc::new(Int64Array::from(vec![-100i64, 
-200, -300]));
+        unsafe {
+            let row0 =
+                std::slice::from_raw_parts(ptr.add(offsets[0] as usize), 
lengths[0] as usize);
+            assert_eq!(i64::from_le_bytes(row0[0..8].try_into().unwrap()) & 1, 
0);
+            assert_eq!(i64::from_le_bytes(row0[8..16].try_into().unwrap()), 
-1);
+
+            let row1 =
+                std::slice::from_raw_parts(ptr.add(offsets[1] as usize), 
lengths[1] as usize);
+            assert_eq!(i64::from_le_bytes(row1[0..8].try_into().unwrap()) & 1, 
1);
+
+            let row2 =
+                std::slice::from_raw_parts(ptr.add(offsets[2] as usize), 
lengths[2] as usize);
+            assert_eq!(i64::from_le_bytes(row2[0..8].try_into().unwrap()) & 1, 
0);
+            assert_eq!(i64::from_le_bytes(row2[8..16].try_into().unwrap()), 
-3);
+        }
+    }
+
+    #[test]
+    fn test_convert_int64_to_decimal128() {
+        // Int64 → Decimal128 reinterpret. Values [-100, null, -300] with a 
null in
+        // the middle to exercise the arity::unary null-buffer path.
+        let int_array: ArrayRef = 
Arc::new(Int64Array::from(vec![Some(-100i64), None, Some(-300)]));
 
-        // Schema expects Decimal128(10, 2)
         let schema = vec![DataType::Decimal128(10, 2)];
         let mut ctx = ColumnarToRowContext::new(schema, 100);
 
@@ -2634,19 +2664,60 @@ mod tests {
         assert_eq!(offsets.len(), 3);
         assert_eq!(lengths.len(), 3);
 
-        // Verify the decimal values are correct after casting
         unsafe {
-            for (i, expected) in [-100i64, -200, -300].iter().enumerate() {
-                let row =
-                    std::slice::from_raw_parts(ptr.add(offsets[i] as usize), 
lengths[i] as usize);
-                // Field value starts at offset 8 (after null bitset)
-                let value = i64::from_le_bytes(row[8..16].try_into().unwrap());
-                assert_eq!(
-                    value, *expected,
-                    "Row {} should have value {}, got {}",
-                    i, expected, value
-                );
-            }
+            let row0 =
+                std::slice::from_raw_parts(ptr.add(offsets[0] as usize), 
lengths[0] as usize);
+            let null_bitset_0 = 
i64::from_le_bytes(row0[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_0 & 1, 0, "row 0 should not be null");
+            assert_eq!(i64::from_le_bytes(row0[8..16].try_into().unwrap()), 
-100);
+
+            let row1 =
+                std::slice::from_raw_parts(ptr.add(offsets[1] as usize), 
lengths[1] as usize);
+            let null_bitset_1 = 
i64::from_le_bytes(row1[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_1 & 1, 1, "row 1 should be null");
+
+            let row2 =
+                std::slice::from_raw_parts(ptr.add(offsets[2] as usize), 
lengths[2] as usize);
+            let null_bitset_2 = 
i64::from_le_bytes(row2[0..8].try_into().unwrap());
+            assert_eq!(null_bitset_2 & 1, 0, "row 2 should not be null");
+            assert_eq!(i64::from_le_bytes(row2[8..16].try_into().unwrap()), 
-300);
+        }
+    }
+
+    #[test]
+    fn test_convert_int64_to_decimal128_sliced() {
+        let full = Int64Array::from(vec![
+            Some(-9999i64),
+            Some(-9998),
+            Some(-100),
+            None,
+            Some(-300),
+            Some(-9997),
+        ]);
+        let sliced: ArrayRef = Arc::new(full.slice(2, 3));
+
+        let schema = vec![DataType::Decimal128(10, 2)];
+        let mut ctx = ColumnarToRowContext::new(schema, 100);
+        let arrays = vec![sliced];
+        let (ptr, offsets, lengths) = ctx.convert(&arrays, 3).unwrap();
+
+        assert!(!ptr.is_null());
+        assert_eq!(offsets.len(), 3);
+
+        unsafe {
+            let row0 =
+                std::slice::from_raw_parts(ptr.add(offsets[0] as usize), 
lengths[0] as usize);
+            assert_eq!(i64::from_le_bytes(row0[0..8].try_into().unwrap()) & 1, 
0);
+            assert_eq!(i64::from_le_bytes(row0[8..16].try_into().unwrap()), 
-100);
+
+            let row1 =
+                std::slice::from_raw_parts(ptr.add(offsets[1] as usize), 
lengths[1] as usize);
+            assert_eq!(i64::from_le_bytes(row1[0..8].try_into().unwrap()) & 1, 
1);
+
+            let row2 =
+                std::slice::from_raw_parts(ptr.add(offsets[2] as usize), 
lengths[2] as usize);
+            assert_eq!(i64::from_le_bytes(row2[0..8].try_into().unwrap()) & 1, 
0);
+            assert_eq!(i64::from_le_bytes(row2[8..16].try_into().unwrap()), 
-300);
         }
     }
 }
diff --git a/native/spark-expr/Cargo.toml b/native/spark-expr/Cargo.toml
index cd7cd89849..2333d2f0e6 100644
--- a/native/spark-expr/Cargo.toml
+++ b/native/spark-expr/Cargo.toml
@@ -215,6 +215,10 @@ harness = false
 name = "unscaled_value"
 harness = false
 
+[[bench]]
+name = "make_decimal"
+harness = false
+
 [[bench]]
 name = "cast_int_to_decimal"
 harness = false
\ No newline at end of file
diff --git a/native/spark-expr/benches/make_decimal.rs 
b/native/spark-expr/benches/make_decimal.rs
new file mode 100644
index 0000000000..c1cc7fef1d
--- /dev/null
+++ b/native/spark-expr/benches/make_decimal.rs
@@ -0,0 +1,164 @@
+// 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 arrow::array::{ArrayRef, Int64Array};
+use arrow::datatypes::DataType;
+use criterion::{criterion_group, criterion_main, Criterion};
+use datafusion::physical_plan::ColumnarValue;
+use datafusion_comet_spark_expr::spark_make_decimal;
+use std::hint::black_box;
+use std::sync::Arc;
+
+// Target Decimal128(18, 2): the widest precision DecimalAggregates produces 
for MakeDecimal.
+// At precision 18 every i64 fits, so every value in these benches is in the 
no-overflow
+// common path.
+const NO_OVERFLOW_PRECISION: u8 = 18;
+const NO_OVERFLOW_SCALE: i8 = 2;
+
+// Narrow target used only for the overflow shapes. Values with |v| >= 1000 do 
not fit.
+const OVERFLOW_PRECISION: u8 = 3;
+const OVERFLOW_SCALE: i8 = 0;
+const OVERFLOW_VALUE: i64 = 1000;
+
+/// Build an Int64 column of `rows` rows, with every `null_every`-th row null
+/// (`null_every == 0` means no nulls). Values always fit Decimal128(18, 2).
+fn create_int64_array(rows: usize, null_every: usize) -> ArrayRef {
+    let arr: Int64Array = (0..rows)
+        .map(|i| {
+            if null_every != 0 && i % null_every == 0 {
+                None
+            } else {
+                Some((i as i64 % 100_000) * 100)
+            }
+        })
+        .collect();
+    Arc::new(arr)
+}
+
+/// Build an Int64 column where every row overflows Decimal128(3, 0).
+fn create_all_overflow_array(rows: usize) -> ArrayRef {
+    let arr: Int64Array = (0..rows).map(|_| Some(OVERFLOW_VALUE)).collect();
+    Arc::new(arr)
+}
+
+/// Build an Int64 column where only the last row overflows Decimal128(3, 0).
+fn create_last_row_overflow_array(rows: usize) -> ArrayRef {
+    let arr: Int64Array = (0..rows)
+        .map(|i| {
+            if i + 1 == rows {
+                Some(OVERFLOW_VALUE)
+            } else {
+                Some((i as i64 % 999) + 1)
+            }
+        })
+        .collect();
+    Arc::new(arr)
+}
+
+fn criterion_benchmark(c: &mut Criterion) {
+    let rows = 8192;
+    let no_overflow_target = DataType::Decimal128(NO_OVERFLOW_PRECISION, 
NO_OVERFLOW_SCALE);
+    let overflow_target = DataType::Decimal128(OVERFLOW_PRECISION, 
OVERFLOW_SCALE);
+
+    let no_nulls = create_int64_array(rows, 0);
+    let sparse_nulls = create_int64_array(rows, 10);
+    let dense_nulls = create_int64_array(rows, 2);
+    let all_overflow = create_all_overflow_array(rows);
+    let last_overflow = create_last_row_overflow_array(rows);
+
+    // Success / non-ANSI paths: unwrap like the neighbouring expression 
benches.
+    // Two-arg helpers match `unscaled_value.rs` (name + array; 
target/`fail_on_error` captured).
+    {
+        let target = no_overflow_target.clone();
+        let mut bench = |name: &str, arr: &ArrayRef| {
+            let args = vec![ColumnarValue::Array(Arc::clone(arr))];
+            let target = target.clone();
+            c.bench_function(name, move |b| {
+                b.iter(|| {
+                    black_box(
+                        spark_make_decimal(black_box(&args), 
black_box(&target), false).unwrap(),
+                    )
+                })
+            });
+        };
+        bench("spark_make_decimal: no nulls", &no_nulls);
+        bench("spark_make_decimal: sparse nulls", &sparse_nulls);
+        bench("spark_make_decimal: dense nulls", &dense_nulls);
+    }
+    {
+        let target = no_overflow_target.clone();
+        let mut bench = |name: &str, arr: &ArrayRef| {
+            let args = vec![ColumnarValue::Array(Arc::clone(arr))];
+            let target = target.clone();
+            c.bench_function(name, move |b| {
+                b.iter(|| {
+                    black_box(
+                        spark_make_decimal(black_box(&args), 
black_box(&target), true).unwrap(),
+                    )
+                })
+            });
+        };
+        bench("spark_make_decimal: ansi no nulls", &no_nulls);
+        bench("spark_make_decimal: ansi sparse nulls", &sparse_nulls);
+        bench("spark_make_decimal: ansi dense nulls", &dense_nulls);
+    }
+    {
+        // Non-ANSI overflow: short-circuiting scan then 
`null_if_overflow_precision`.
+        let target = overflow_target.clone();
+        let mut bench = |name: &str, arr: &ArrayRef| {
+            let args = vec![ColumnarValue::Array(Arc::clone(arr))];
+            let target = target.clone();
+            c.bench_function(name, move |b| {
+                b.iter(|| {
+                    black_box(
+                        spark_make_decimal(black_box(&args), 
black_box(&target), false).unwrap(),
+                    )
+                })
+            });
+        };
+        bench("spark_make_decimal: overflow all rows", &all_overflow);
+        bench("spark_make_decimal: overflow last row", &last_overflow);
+    }
+
+    // ANSI overflow raises; black-box the Result (same idea as check_overflow 
only
+    // unwrapping non-error paths, but here the error path itself is the shape 
under test).
+    let ansi_last_args = 
vec![ColumnarValue::Array(Arc::clone(&last_overflow))];
+    let ansi_last_target = overflow_target.clone();
+    c.bench_function("spark_make_decimal: ansi overflow last row", |b| {
+        b.iter(|| {
+            black_box(spark_make_decimal(
+                black_box(&ansi_last_args),
+                black_box(&ansi_last_target),
+                true,
+            ))
+        })
+    });
+
+    let ansi_all_args = vec![ColumnarValue::Array(Arc::clone(&all_overflow))];
+    c.bench_function("spark_make_decimal: ansi overflow all rows", |b| {
+        b.iter(|| {
+            black_box(spark_make_decimal(
+                black_box(&ansi_all_args),
+                black_box(&overflow_target),
+                true,
+            ))
+        })
+    });
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
diff --git a/native/spark-expr/src/math_funcs/internal/make_decimal.rs 
b/native/spark-expr/src/math_funcs/internal/make_decimal.rs
index 1961ffd66b..13d677999a 100644
--- a/native/spark-expr/src/math_funcs/internal/make_decimal.rs
+++ b/native/spark-expr/src/math_funcs/internal/make_decimal.rs
@@ -17,10 +17,11 @@
 
 use crate::error::decimal_overflow_error;
 use crate::math_funcs::utils::get_precision_scale;
+use arrow::compute::kernels::arity::unary;
 use arrow::datatypes::DataType;
 use arrow::{
-    array::{AsArray, Decimal128Builder},
-    datatypes::{validate_decimal_precision, Int64Type},
+    array::{AsArray, Decimal128Array},
+    datatypes::{validate_decimal_precision, Decimal128Type, DecimalType, 
Int64Type},
 };
 use datafusion::common::{internal_err, DataFusionError, Result as 
DataFusionResult, ScalarValue};
 use datafusion::physical_plan::ColumnarValue;
@@ -45,15 +46,34 @@ pub fn spark_make_decimal(
         ColumnarValue::Array(a) => match a.data_type() {
             DataType::Int64 => {
                 let arr = a.as_primitive::<Int64Type>();
-                let mut result = Decimal128Builder::new();
-                for v in arr.into_iter() {
-                    result.append_option(long_to_decimal(v, precision, scale, 
fail_on_error)?)
-                }
                 let result_type = DataType::Decimal128(precision, scale);
 
-                Ok(ColumnarValue::Array(Arc::new(
-                    result.finish().with_data_type(result_type),
-                )))
+                // The Int64 is already the unscaled Decimal128 value; this 
widens the bits
+                // (an Arrow Int64->Decimal cast would rescale). Infallible so 
it vectorizes.
+                let widened: Decimal128Array = unary::<_, _, 
Decimal128Type>(arr, |v| v as i128);
+
+                // `.iter().flatten()` skips null slots so garbage under a 
null cannot
+                // trigger a false overflow. `find` short-circuits like 
`.all(is_valid)`
+                // while also handing back the value needed for the ANSI error 
message.
+                let first_offender = widened
+                    .iter()
+                    .flatten()
+                    .find(|v| !Decimal128Type::is_valid_decimal_precision(*v, 
precision));
+
+                let result = match (first_offender, fail_on_error) {
+                    // No overflow: attach metadata. 
`with_precision_and_scale` would rescan.
+                    (None, _) => widened.with_data_type(result_type),
+                    (Some(v), true) => {
+                        return 
Err(DataFusionError::External(Box::new(decimal_overflow_error(
+                            v, precision, scale,
+                        ))));
+                    }
+                    (Some(_), false) => widened
+                        .null_if_overflow_precision(precision)
+                        .with_data_type(result_type),
+                };
+
+                Ok(ColumnarValue::Array(Arc::new(result)))
             }
             av => internal_err!("Expected Int64 but found {av:?}"),
         },
@@ -85,7 +105,7 @@ fn long_to_decimal(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use arrow::array::{Array, Int64Array};
+    use arrow::array::{Array, Decimal128Array, Int64Array};
 
     fn overflow_args() -> [ColumnarValue; 1] {
         // 123456 does not fit Decimal128(3, 0)
@@ -106,6 +126,30 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_array_overflow_reports_first_offending_value() {
+        // Two distinct overflowing values with a valid value in front. The
+        // `.iter().flatten().find(...)` scan walks rows in index order, so 
the reported
+        // error must reference the FIRST overflow (111111), not the later one 
(222222).
+        // This locks the ordering semantics rather than merely "some error 
occurred".
+        let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            Some(99),
+            Some(111111),
+            Some(222222),
+        ])))];
+        let err = spark_make_decimal(&args, &DataType::Decimal128(3, 0), true)
+            .expect_err("overflow should error when fail_on_error is set");
+        let msg = err.to_string();
+        assert!(
+            msg.contains("111111"),
+            "should report first overflow: {msg}"
+        );
+        assert!(
+            !msg.contains("222222"),
+            "should not report later overflow: {msg}"
+        );
+    }
+
     #[test]
     fn test_array_overflow_nulls_when_not_fail_on_error() {
         let result = spark_make_decimal(&overflow_args(), 
&DataType::Decimal128(3, 0), false)
@@ -139,4 +183,159 @@ mod tests {
         };
         assert!(v.is_none());
     }
+
+    #[test]
+    fn test_scalar_valid_value() {
+        // Locks the happy path for the scalar branch: an in-range value 
round-trips
+        // to the correct Decimal128 with matching precision/scale. Existing 
scalar
+        // tests only cover overflow and null input.
+        let args = [ColumnarValue::Scalar(ScalarValue::Int64(Some(999)))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
true)
+            .expect("in-range scalar should succeed");
+        let ColumnarValue::Scalar(ScalarValue::Decimal128(v, 3, 0)) = result 
else {
+            panic!("expected decimal scalar result")
+        };
+        assert_eq!(v, Some(999));
+    }
+
+    #[test]
+    fn test_scalar_overflow_nulls_when_not_fail_on_error() {
+        // Covers `long_to_decimal`'s `Err(_) => Ok(None)` branch through the 
scalar
+        // path. The array path exercises this branch indirectly via 
`null_if_overflow_precision`;
+        // the scalar branch has its own code and was previously untested.
+        let args = [ColumnarValue::Scalar(ScalarValue::Int64(Some(123456)))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("scalar overflow without fail_on_error should return 
null");
+        let ColumnarValue::Scalar(ScalarValue::Decimal128(v, 3, 0)) = result 
else {
+            panic!("expected decimal scalar result")
+        };
+        assert_eq!(v, None);
+    }
+
+    #[test]
+    fn test_array_no_overflow_fast_path() {
+        // Common path: no overflow, no nulls. Values pass through unchanged 
and the
+        // output carries the target Decimal128 type. Locks the (None, _) arm 
that
+        // existing overflow tests never touch.
+        let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            Some(1i64),
+            Some(50),
+            Some(999),
+        ])))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("no overflow should succeed");
+        let ColumnarValue::Array(array) = result else {
+            panic!("expected array result")
+        };
+        assert_eq!(array.data_type(), &DataType::Decimal128(3, 0));
+        assert_eq!(array.null_count(), 0);
+        let decimals = 
array.as_any().downcast_ref::<Decimal128Array>().unwrap();
+        assert_eq!(decimals.value(0), 1);
+        assert_eq!(decimals.value(1), 50);
+        assert_eq!(decimals.value(2), 999);
+    }
+
+    #[test]
+    fn test_array_negative_overflow_nulls() {
+        // `is_valid_decimal_precision` checks both bounds; -1000 is below the
+        // Decimal128(3, 0) lower bound (-999). Guards the negative branch that
+        // existing overflow tests (all positive) do not cover.
+        let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            Some(-1000i64),
+            Some(5),
+        ])))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("negative overflow should become null");
+        let ColumnarValue::Array(array) = result else {
+            panic!("expected array result")
+        };
+        assert!(array.is_null(0), "negative overflow should be nulled");
+        assert!(array.is_valid(1));
+    }
+
+    #[test]
+    fn test_array_null_slot_garbage_not_scanned() {
+        // Regression guard for the `.iter().flatten()` decision. `unary` 
widens
+        // every slot including nulls, so the null slot's underlying storage 
can
+        // hold arbitrary bits. If the scan ever switched to 
`.values().iter()`,
+        // i64::MAX under the null would falsely trip Decimal128(3, 0) 
overflow.
+        use arrow::buffer::{NullBuffer, ScalarBuffer};
+
+        let values = ScalarBuffer::from(vec![10i64, i64::MAX, 20i64]);
+        let nulls = NullBuffer::from(vec![true, false, true]);
+        let arr = Int64Array::new(values, Some(nulls));
+        let args = [ColumnarValue::Array(Arc::new(arr))];
+
+        // Non-ANSI: no overflow detected, null slot stays null (not 
double-nullified).
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("null slot garbage must not trigger overflow");
+        let ColumnarValue::Array(array) = result else {
+            panic!("expected array result")
+        };
+        assert!(array.is_valid(0));
+        assert!(array.is_null(1));
+        assert!(array.is_valid(2));
+
+        // ANSI: also must not raise on garbage hidden behind a null.
+        spark_make_decimal(&args, &DataType::Decimal128(3, 0), true)
+            .expect("ANSI mode must not error on null slot garbage");
+    }
+
+    #[test]
+    fn test_array_boundary_precision() {
+        // 999 is exactly the max for Decimal128(3, 0); 1000 is over by one.
+        // Pins the off-by-one on `is_valid_decimal_precision`.
+        let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            Some(999i64),
+            Some(1000),
+        ])))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("boundary case should not error in non-ANSI");
+        let ColumnarValue::Array(array) = result else {
+            panic!("expected array result")
+        };
+        assert!(array.is_valid(0));
+        assert!(array.is_null(1));
+    }
+
+    #[test]
+    fn test_scalar_boundary_precision() {
+        // Same off-by-one as `test_array_boundary_precision`, but through the 
scalar path
+        // (`validate_decimal_precision`). Locks the two Arrow entry points 
together.
+        let ok = [ColumnarValue::Scalar(ScalarValue::Int64(Some(999)))];
+        let result = spark_make_decimal(&ok, &DataType::Decimal128(3, 0), 
false)
+            .expect("999 should fit Decimal128(3, 0)");
+        let ColumnarValue::Scalar(ScalarValue::Decimal128(v, 3, 0)) = result 
else {
+            panic!("expected decimal scalar result")
+        };
+        assert_eq!(v, Some(999));
+
+        let over = [ColumnarValue::Scalar(ScalarValue::Int64(Some(1000)))];
+        let result = spark_make_decimal(&over, &DataType::Decimal128(3, 0), 
false)
+            .expect("1000 should null without fail_on_error");
+        let ColumnarValue::Scalar(ScalarValue::Decimal128(v, 3, 0)) = result 
else {
+            panic!("expected decimal scalar result")
+        };
+        assert_eq!(v, None);
+    }
+
+    #[test]
+    fn test_array_all_null() {
+        // .iter().flatten() on an all-null array yields nothing, so `find` 
returns
+        // None and the fast path is taken. Locks that the all-null mask and 
target
+        // type are preserved.
+        let args = [ColumnarValue::Array(Arc::new(Int64Array::from(vec![
+            None::<i64>,
+            None,
+            None,
+        ])))];
+        let result = spark_make_decimal(&args, &DataType::Decimal128(3, 0), 
false)
+            .expect("all-null should succeed");
+        let ColumnarValue::Array(array) = result else {
+            panic!("expected array result")
+        };
+        assert_eq!(array.data_type(), &DataType::Decimal128(3, 0));
+        assert_eq!(array.len(), 3);
+        assert_eq!(array.null_count(), 3);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to