You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/04/03 21:19:00 UTC

(datafusion) branch main updated: feat(spark): Adds spark round function (#21062)

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

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


The following commit(s) were added to refs/heads/main by this push:
     new d143bfa95e feat(spark): Adds spark round function (#21062)
d143bfa95e is described below

commit d143bfa95e7832407017d3e33e063039aa2b9b4a
Author: Subham Singhal <Su...@users.noreply.github.com>
AuthorDate: Sat Apr 4 02:47:15 2026 +0530

    feat(spark): Adds spark round function (#21062)
    
    ## What changes are included in this PR?
    
    Adds support for round spark function in data fusion.
    
    ## Are these changes tested?
    
    yes, using UTs
    
    ## Are there any user-facing changes?
    
    yes, adds new function.
    
    ---------
    
    Co-authored-by: Subham Singhal <su...@Subhams-MacBook-Air.local>
    Co-authored-by: Martin Grigorov <ma...@users.noreply.github.com>
    Co-authored-by: Andrew Lamb <an...@nerdnetworks.org>
---
 Cargo.lock                                         |   1 +
 datafusion/spark/Cargo.toml                        |   1 +
 datafusion/spark/src/function/math/mod.rs          |   8 +
 datafusion/spark/src/function/math/round.rs        | 654 +++++++++++++++++++++
 .../sqllogictest/test_files/spark/math/round.slt   | 566 +++++++++++++++++-
 5 files changed, 1224 insertions(+), 6 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 4803d13bca..895b3059f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2603,6 +2603,7 @@ dependencies = [
  "datafusion-functions-aggregate",
  "datafusion-functions-nested",
  "log",
+ "num-traits",
  "percent-encoding",
  "rand 0.9.2",
  "serde_json",
diff --git a/datafusion/spark/Cargo.toml b/datafusion/spark/Cargo.toml
index bd5f2bb18a..0651fe5651 100644
--- a/datafusion/spark/Cargo.toml
+++ b/datafusion/spark/Cargo.toml
@@ -57,6 +57,7 @@ datafusion-functions = { workspace = true, features = ["crypto_expressions"] }
 datafusion-functions-aggregate = { workspace = true }
 datafusion-functions-nested = { workspace = true }
 log = { workspace = true }
+num-traits = { workspace = true }
 percent-encoding = "2.3.2"
 rand = { workspace = true }
 serde_json = { workspace = true }
diff --git a/datafusion/spark/src/function/math/mod.rs b/datafusion/spark/src/function/math/mod.rs
index 7f7d04e06b..be3f34d332 100644
--- a/datafusion/spark/src/function/math/mod.rs
+++ b/datafusion/spark/src/function/math/mod.rs
@@ -23,6 +23,7 @@ pub mod hex;
 pub mod modulus;
 pub mod negative;
 pub mod rint;
+pub mod round;
 pub mod trigonometry;
 pub mod unhex;
 pub mod width_bucket;
@@ -38,6 +39,7 @@ make_udf_function!(hex::SparkHex, hex);
 make_udf_function!(modulus::SparkMod, modulus);
 make_udf_function!(modulus::SparkPmod, pmod);
 make_udf_function!(rint::SparkRint, rint);
+make_udf_function!(round::SparkRound, round);
 make_udf_function!(unhex::SparkUnhex, unhex);
 make_udf_function!(width_bucket::SparkWidthBucket, width_bucket);
 make_udf_function!(trigonometry::SparkCsc, csc);
@@ -63,6 +65,11 @@ pub mod expr_fn {
         "Returns the double value that is closest in value to the argument and is equal to a mathematical integer.",
         arg1
     ));
+    export_functions!((
+        round,
+        "Rounds the value of expr to scale decimal places using HALF_UP rounding mode.",
+        arg1 arg2
+    ));
     export_functions!((unhex, "Converts hexadecimal string to binary.", arg1));
     export_functions!((width_bucket, "Returns the bucket number into which the value of this expression would fall after being evaluated.", arg1 arg2 arg3 arg4));
     export_functions!((csc, "Returns the cosecant of expr.", arg1));
@@ -88,6 +95,7 @@ pub fn functions() -> Vec<Arc<ScalarUDF>> {
         modulus(),
         pmod(),
         rint(),
+        round(),
         unhex(),
         width_bucket(),
         csc(),
diff --git a/datafusion/spark/src/function/math/round.rs b/datafusion/spark/src/function/math/round.rs
new file mode 100644
index 0000000000..0574566618
--- /dev/null
+++ b/datafusion/spark/src/function/math/round.rs
@@ -0,0 +1,654 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use arrow::array::*;
+use arrow::datatypes::{
+    ArrowNativeTypeOp, DataType, Decimal32Type, Decimal64Type, Decimal128Type,
+    Decimal256Type, Float16Type, Float32Type, Float64Type, Int8Type, Int16Type,
+    Int32Type, Int64Type, UInt8Type, UInt16Type, UInt32Type, UInt64Type,
+};
+use datafusion_common::types::{
+    NativeType, logical_float32, logical_float64, logical_int32,
+};
+use datafusion_common::{Result, ScalarValue, exec_err, not_impl_err};
+use datafusion_expr::{
+    Coercion, ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature,
+    TypeSignatureClass, Volatility,
+};
+
+/// Spark-compatible `round` expression
+/// <https://spark.apache.org/docs/latest/api/sql/index.html#round>
+///
+/// Rounds the value of `expr` to `scale` decimal places using HALF_UP rounding mode.
+/// Returns the same type as the input expression.
+///
+/// - `round(expr)` rounds to 0 decimal places (default scale = 0)
+/// - `round(expr, scale)` rounds to `scale` decimal places
+/// - For integer types with negative scale: `round(25, -1)` → `30`
+/// - Uses HALF_UP rounding: 2.5 → 3, -2.5 → -3 (away from zero)
+///
+/// Supported types: Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64,
+/// Float16, Float32, Float64, Decimal32, Decimal64, Decimal128, Decimal256
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub struct SparkRound {
+    signature: Signature,
+}
+
+impl Default for SparkRound {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl SparkRound {
+    pub fn new() -> Self {
+        let decimal = Coercion::new_exact(TypeSignatureClass::Decimal);
+        let integer = Coercion::new_exact(TypeSignatureClass::Integer);
+        let decimal_places = Coercion::new_implicit(
+            TypeSignatureClass::Native(logical_int32()),
+            vec![TypeSignatureClass::Integer],
+            NativeType::Int32,
+        );
+        let float32 = Coercion::new_exact(TypeSignatureClass::Native(logical_float32()));
+        let float64 = Coercion::new_implicit(
+            TypeSignatureClass::Native(logical_float64()),
+            vec![TypeSignatureClass::Numeric],
+            NativeType::Float64,
+        );
+        Self {
+            signature: Signature::one_of(
+                vec![
+                    // round(decimal, scale)
+                    TypeSignature::Coercible(vec![
+                        decimal.clone(),
+                        decimal_places.clone(),
+                    ]),
+                    // round(decimal)
+                    TypeSignature::Coercible(vec![decimal]),
+                    // round(integer, scale)
+                    TypeSignature::Coercible(vec![
+                        integer.clone(),
+                        decimal_places.clone(),
+                    ]),
+                    // round(integer)
+                    TypeSignature::Coercible(vec![integer]),
+                    // round(float32, scale)
+                    TypeSignature::Coercible(vec![
+                        float32.clone(),
+                        decimal_places.clone(),
+                    ]),
+                    // round(float32)
+                    TypeSignature::Coercible(vec![float32]),
+                    // round(float64, scale)
+                    TypeSignature::Coercible(vec![float64.clone(), decimal_places]),
+                    // round(float64)
+                    TypeSignature::Coercible(vec![float64]),
+                ],
+                Volatility::Immutable,
+            ),
+        }
+    }
+}
+
+impl ScalarUDFImpl for SparkRound {
+    fn name(&self) -> &str {
+        "round"
+    }
+
+    fn signature(&self) -> &Signature {
+        &self.signature
+    }
+
+    fn return_type(&self, arg_types: &[DataType]) -> Result<DataType> {
+        Ok(arg_types[0].clone())
+    }
+
+    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
+        spark_round(&args.args, args.config_options.execution.enable_ansi_mode)
+    }
+}
+
+/// Extract the scale (decimal places) from the second argument.
+/// Returns `Some(0)` if no second argument is provided.
+/// Returns `None` if the scale argument is NULL (Spark returns NULL for `round(expr, NULL)`).
+fn get_scale(args: &[ColumnarValue]) -> Result<Option<i32>> {
+    if args.len() < 2 {
+        return Ok(Some(0));
+    }
+
+    match &args[1] {
+        ColumnarValue::Scalar(ScalarValue::Int8(Some(v))) => Ok(Some(i32::from(*v))),
+        ColumnarValue::Scalar(ScalarValue::Int16(Some(v))) => Ok(Some(i32::from(*v))),
+        ColumnarValue::Scalar(ScalarValue::Int32(Some(v))) => Ok(Some(*v)),
+        ColumnarValue::Scalar(ScalarValue::Int64(Some(v))) => {
+            i32::try_from(*v).map(Some).map_err(|_| {
+                (exec_err!("round scale {v} is out of supported i32 range")
+                    as Result<(), _>)
+                    .unwrap_err()
+            })
+        }
+        ColumnarValue::Scalar(ScalarValue::UInt8(Some(v))) => Ok(Some(i32::from(*v))),
+        ColumnarValue::Scalar(ScalarValue::UInt16(Some(v))) => Ok(Some(i32::from(*v))),
+        ColumnarValue::Scalar(ScalarValue::UInt32(Some(v))) => {
+            i32::try_from(*v).map(Some).map_err(|_| {
+                (exec_err!("round scale {v} is out of supported i32 range")
+                    as Result<(), _>)
+                    .unwrap_err()
+            })
+        }
+        ColumnarValue::Scalar(ScalarValue::UInt64(Some(v))) => {
+            i32::try_from(*v).map(Some).map_err(|_| {
+                (exec_err!("round scale {v} is out of supported i32 range")
+                    as Result<(), _>)
+                    .unwrap_err()
+            })
+        }
+        ColumnarValue::Scalar(sv) if sv.is_null() => Ok(None),
+        other => exec_err!("Unsupported type for round scale: {}", other.data_type()),
+    }
+}
+
+/// Round a floating-point value to the given number of decimal places using
+/// HALF_UP rounding mode (ties round away from zero).
+///
+/// This matches Spark's `RoundBase` behaviour for `FloatType` / `DoubleType`,
+/// which internally converts the value to `BigDecimal` and rounds with
+/// `RoundingMode.HALF_UP`.
+///
+/// # Arguments
+/// * `value` – the floating-point number to round
+/// * `scale` – number of decimal places to keep.
+///   - `scale >= 0`: rounds to that many fractional digits
+///     (e.g. `round_float(2.345, 2) == 2.35`)
+///   - `scale < 0`:  rounds to the left of the decimal point
+///     (e.g. `round_float(125.0, -1) == 130.0`)
+///
+/// # Examples
+/// ```text
+/// round_float(2.5,  0) →  3.0   // half rounds up
+/// round_float(-2.5, 0) → -3.0   // half rounds away from zero
+/// round_float(1.4,  0) →  1.0
+/// round_float(125.0, -1) → 130.0
+/// ```
+fn round_float<T: num_traits::Float>(value: T, scale: i32) -> T {
+    if scale >= 0 {
+        let factor = T::from(10.0f64.powi(scale)).unwrap_or_else(T::infinity);
+        if factor.is_infinite() {
+            // Very large positive scale — value is already precise enough, return as-is
+            return value;
+        }
+        (value * factor).round() / factor
+    } else {
+        let factor = T::from(10.0f64.powi(-scale)).unwrap_or_else(T::infinity);
+        if factor.is_infinite() {
+            // Very large negative scale — any finite value rounds to 0
+            return T::zero();
+        }
+        (value / factor).round() * factor
+    }
+}
+
+/// Round an integer value to the given scale using HALF_UP rounding mode.
+///
+/// Only meaningful when `scale` is negative — a non-negative scale leaves
+/// the integer unchanged because integers have no fractional part.
+///
+/// This matches Spark's `RoundBase` behaviour for `ByteType`, `ShortType`,
+/// `IntegerType`, and `LongType`, which round to the nearest power-of-ten
+/// boundary and return the same integer type.
+///
+/// In ANSI mode, overflow conditions return an error instead of wrapping.
+///
+/// # Arguments
+/// * `value` – the integer to round (widened to `i64` by callers)
+/// * `scale` – rounding position relative to the ones digit.
+///   - `scale >= 0`:  returns `value` as-is
+///   - `scale == -1`: rounds to the nearest 10
+///   - `scale == -2`: rounds to the nearest 100
+///   - If `10^|scale|` overflows `i64`, returns `0`
+/// * `enable_ansi_mode` – when true, overflow returns an error
+///
+/// # Examples
+/// ```text
+/// round_integer(25,   -1, false) →  Ok(30)
+/// round_integer(-25,  -1, false) → Ok(-30)
+/// round_integer(123,  -1, false) →  Ok(120)
+/// round_integer(150,  -2, false) →  Ok(200)
+/// round_integer(42,    2, false) →   Ok(42)   // no-op for positive scale
+/// round_integer(42,  -10, false) →    Ok(0)   // factor overflows → 0
+/// ```
+fn round_integer(value: i64, scale: i32, enable_ansi_mode: bool) -> Result<i64> {
+    if scale >= 0 {
+        return Ok(value);
+    }
+    let abs_scale = (-scale) as u32;
+    let Some(factor) = 10_i64.checked_pow(abs_scale) else {
+        return Ok(0);
+    };
+    let remainder = value % factor;
+    let threshold = factor / 2;
+    let result = if remainder >= threshold {
+        if enable_ansi_mode {
+            value
+                .checked_sub(remainder)
+                .and_then(|v| v.checked_add(factor))
+                .ok_or_else(|| {
+                    (exec_err!("Int64 overflow on round({value}, {scale})")
+                        as Result<(), _>)
+                        .unwrap_err()
+                })?
+        } else {
+            value.wrapping_sub(remainder).wrapping_add(factor)
+        }
+    } else if remainder <= -threshold {
+        if enable_ansi_mode {
+            value
+                .checked_sub(remainder)
+                .and_then(|v| v.checked_sub(factor))
+                .ok_or_else(|| {
+                    (exec_err!("Int64 overflow on round({value}, {scale})")
+                        as Result<(), _>)
+                        .unwrap_err()
+                })?
+        } else {
+            value.wrapping_sub(remainder).wrapping_sub(factor)
+        }
+    } else {
+        value - remainder
+    };
+    Ok(result)
+}
+
+// ---------------------------------------------------------------------------
+// Decimal rounding using ArrowNativeTypeOp (HALF_UP)
+// ---------------------------------------------------------------------------
+
+/// Round a decimal value represented as its unscaled integer using HALF_UP
+/// rounding mode (ties round away from zero).
+///
+/// This matches Spark's `RoundBase` behaviour for `DecimalType`, which calls
+/// `BigDecimal.setScale(scale, RoundingMode.HALF_UP)`.
+///
+/// Decimals are stored as `(unscaled_value, precision, scale)` where the real
+/// value equals `unscaled_value * 10^(-scale)`.  This function operates on the
+/// unscaled integer directly:
+///
+/// 1. Compute `diff = input_scale - decimal_places`.
+///    If `diff <= 0` the requested precision is finer than (or equal to) the
+///    stored scale, so nothing needs to be rounded — return as-is.
+/// 2. Divide by `10^diff` to shift the rounding boundary into the ones digit.
+/// 3. Inspect the remainder to decide whether to round up or down (HALF_UP).
+/// 4. Multiply back by `10^diff` so the result is expressed at the original
+///    `input_scale`.
+///
+/// # Arguments
+/// * `value`          – unscaled decimal value
+/// * `input_scale`    – scale of the incoming decimal
+/// * `decimal_places` – number of fractional digits to keep (may be negative)
+///
+/// # Returns
+/// The rounded unscaled value at the same `input_scale`, or an error
+/// on overflow.
+///
+/// # Examples
+/// ```text
+/// // 2.5 (unscaled 25, scale 1) rounded to 0 places → 3.0 (unscaled 30)
+/// round_decimal(25_i128, 1, 0)  → Ok(30)
+///
+/// // 2.345 (unscaled 2345, scale 3) rounded to 2 places → 2.350 (unscaled 2350)
+/// round_decimal(2345_i128, 3, 2) → Ok(2350)
+/// ```
+fn round_decimal<V: ArrowNativeTypeOp>(
+    value: V,
+    input_scale: i8,
+    decimal_places: i32,
+) -> Result<V> {
+    let diff = i64::from(input_scale) - i64::from(decimal_places);
+    if diff <= 0 {
+        // Nothing to round – the requested precision is finer than (or equal to) the
+        // stored scale.
+        return Ok(value);
+    }
+
+    let diff = diff as u32;
+
+    let one = V::ONE;
+    let two = V::from_usize(2).ok_or_else(|| {
+        (exec_err!("Internal error: could not create constant 2") as Result<(), _>)
+            .unwrap_err()
+    })?;
+    let ten = V::from_usize(10).ok_or_else(|| {
+        (exec_err!("Internal error: could not create constant 10") as Result<(), _>)
+            .unwrap_err()
+    })?;
+
+    let Ok(factor) = ten.pow_checked(diff) else {
+        // 10^diff overflows the decimal type — the rounding position is beyond
+        // the representable range, so any value rounds to 0.
+        // This matches Spark's BigDecimal.setScale behavior where rounding to a
+        // scale far beyond the number's magnitude yields 0.
+        return Ok(V::ZERO);
+    };
+
+    let mut quotient = value.div_wrapping(factor);
+    let remainder = value.mod_wrapping(factor);
+
+    // HALF_UP: round away from zero when remainder is exactly half
+    let threshold = factor.div_wrapping(two);
+    if remainder >= threshold {
+        quotient = quotient.add_checked(one).map_err(|_| {
+            (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err()
+        })?;
+    } else if remainder <= threshold.neg_wrapping() {
+        quotient = quotient.sub_checked(one).map_err(|_| {
+            (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err()
+        })?;
+    }
+
+    // Re-scale the quotient back to `input_scale` so the returned unscaled integer is
+    // at the original scale. `factor` is already `10^diff` which is exactly the shift
+    // we need.
+    quotient.mul_checked(factor).map_err(|_| {
+        (exec_err!("Overflow while rounding decimal") as Result<(), _>).unwrap_err()
+    })
+}
+
+// ---------------------------------------------------------------------------
+// Macros for array dispatch
+// ---------------------------------------------------------------------------
+
+macro_rules! impl_integer_array_round {
+    ($array:expr, $arrow_type:ty, $scale:expr, $enable_ansi_mode:expr) => {{
+        let array = $array.as_primitive::<$arrow_type>();
+        type Native = <$arrow_type as arrow::datatypes::ArrowPrimitiveType>::Native;
+        let result: PrimitiveArray<$arrow_type> = if $enable_ansi_mode {
+            array.try_unary(|x| {
+                let v = round_integer(x as i64, $scale, true)?;
+                Native::try_from(v).map_err(|_| {
+                    (exec_err!(
+                        "{} overflow on round({x}, {})",
+                        stringify!($arrow_type),
+                        $scale
+                    ) as Result<(), _>)
+                        .unwrap_err()
+                })
+            })?
+        } else {
+            array.unary(|x| round_integer(x as i64, $scale, false).unwrap() as Native)
+        };
+        Ok(ColumnarValue::Array(Arc::new(result)))
+    }};
+}
+
+macro_rules! impl_float_array_round {
+    ($array:expr, $arrow_type:ty, $scale:expr) => {{
+        let array = $array.as_primitive::<$arrow_type>();
+        let result: PrimitiveArray<$arrow_type> = array.unary(|x| round_float(x, $scale));
+        Ok(ColumnarValue::Array(Arc::new(result)))
+    }};
+}
+
+macro_rules! impl_decimal_array_round {
+    ($array:expr, $arrow_type:ty, $input_scale:expr, $scale:expr) => {{
+        let array = $array.as_primitive::<$arrow_type>();
+        let result: PrimitiveArray<$arrow_type> = array
+            .try_unary(|x| round_decimal(x, $input_scale, $scale))?
+            .with_data_type($array.data_type().clone());
+        Ok(ColumnarValue::Array(Arc::new(result)))
+    }};
+}
+
+// ---------------------------------------------------------------------------
+// Core dispatch
+// ---------------------------------------------------------------------------
+
+fn spark_round(args: &[ColumnarValue], enable_ansi_mode: bool) -> Result<ColumnarValue> {
+    if args.is_empty() || args.len() > 2 {
+        return exec_err!("round requires 1 or 2 arguments, got {}", args.len());
+    }
+
+    let scale = match get_scale(args)? {
+        Some(s) => s,
+        None => {
+            // NULL scale → return NULL with the same data type as the first argument
+            return Ok(ColumnarValue::Scalar(ScalarValue::try_from(
+                args[0].data_type(),
+            )?));
+        }
+    };
+
+    match &args[0] {
+        ColumnarValue::Array(array) => match array.data_type() {
+            DataType::Null => Ok(args[0].clone()),
+
+            // Integer types
+            DataType::Int8 => {
+                impl_integer_array_round!(array, Int8Type, scale, enable_ansi_mode)
+            }
+            DataType::Int16 => {
+                impl_integer_array_round!(array, Int16Type, scale, enable_ansi_mode)
+            }
+            DataType::Int32 => {
+                impl_integer_array_round!(array, Int32Type, scale, enable_ansi_mode)
+            }
+            DataType::Int64 => {
+                impl_integer_array_round!(array, Int64Type, scale, enable_ansi_mode)
+            }
+
+            // Unsigned integer types
+            DataType::UInt8 => {
+                impl_integer_array_round!(array, UInt8Type, scale, enable_ansi_mode)
+            }
+            DataType::UInt16 => {
+                impl_integer_array_round!(array, UInt16Type, scale, enable_ansi_mode)
+            }
+            DataType::UInt32 => {
+                impl_integer_array_round!(array, UInt32Type, scale, enable_ansi_mode)
+            }
+            DataType::UInt64 => {
+                let array = array.as_primitive::<UInt64Type>();
+                let result: PrimitiveArray<UInt64Type> = array.try_unary(|x| {
+                    let v_i64 = i64::try_from(x).map_err(|_| {
+                        (exec_err!(
+                            "round: UInt64 value {x} exceeds i64::MAX and cannot be rounded"
+                        ) as Result<(), _>)
+                            .unwrap_err()
+                    })?;
+                    round_integer(v_i64, scale, enable_ansi_mode)
+                        .map(|v| v as u64)
+                })?;
+                Ok(ColumnarValue::Array(Arc::new(result)))
+            }
+
+            // Float types
+            DataType::Float16 => impl_float_array_round!(array, Float16Type, scale),
+            DataType::Float32 => impl_float_array_round!(array, Float32Type, scale),
+            DataType::Float64 => impl_float_array_round!(array, Float64Type, scale),
+
+            // Decimal types
+            DataType::Decimal32(_, input_scale) => {
+                impl_decimal_array_round!(array, Decimal32Type, *input_scale, scale)
+            }
+            DataType::Decimal64(_, input_scale) => {
+                impl_decimal_array_round!(array, Decimal64Type, *input_scale, scale)
+            }
+            DataType::Decimal128(_, input_scale) => {
+                impl_decimal_array_round!(array, Decimal128Type, *input_scale, scale)
+            }
+            DataType::Decimal256(_, input_scale) => {
+                impl_decimal_array_round!(array, Decimal256Type, *input_scale, scale)
+            }
+
+            dt => not_impl_err!("Unsupported data type for Spark round(): {dt}"),
+        },
+
+        ColumnarValue::Scalar(sv) => match sv {
+            ScalarValue::Null => Ok(args[0].clone()),
+            _ if sv.is_null() => Ok(args[0].clone()),
+
+            // Integer scalars
+            ScalarValue::Int8(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    i8::try_from(r).map_err(|_| {
+                        (exec_err!("Int8 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as i8
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::Int8(Some(result))))
+            }
+            ScalarValue::Int16(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    i16::try_from(r).map_err(|_| {
+                        (exec_err!("Int16 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as i16
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::Int16(Some(result))))
+            }
+            ScalarValue::Int32(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    i32::try_from(r).map_err(|_| {
+                        (exec_err!("Int32 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as i32
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::Int32(Some(result))))
+            }
+            ScalarValue::Int64(Some(v)) => {
+                let result = round_integer(*v, scale, enable_ansi_mode)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::Int64(Some(result))))
+            }
+
+            // Unsigned integer scalars
+            ScalarValue::UInt8(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    u8::try_from(r).map_err(|_| {
+                        (exec_err!("UInt8 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as u8
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::UInt8(Some(result))))
+            }
+            ScalarValue::UInt16(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    u16::try_from(r).map_err(|_| {
+                        (exec_err!("UInt16 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as u16
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::UInt16(Some(result))))
+            }
+            ScalarValue::UInt32(Some(v)) => {
+                let r = round_integer(i64::from(*v), scale, enable_ansi_mode)?;
+                let result = if enable_ansi_mode {
+                    u32::try_from(r).map_err(|_| {
+                        (exec_err!("UInt32 overflow on round({v}, {scale})")
+                            as Result<(), _>)
+                            .unwrap_err()
+                    })?
+                } else {
+                    r as u32
+                };
+                Ok(ColumnarValue::Scalar(ScalarValue::UInt32(Some(result))))
+            }
+            ScalarValue::UInt64(Some(v)) => {
+                let v_i64 = i64::try_from(*v).map_err(|_| {
+                    (exec_err!(
+                        "round: UInt64 value {v} exceeds i64::MAX and cannot be rounded"
+                    ) as Result<(), _>)
+                        .unwrap_err()
+                })?;
+                let result = round_integer(v_i64, scale, enable_ansi_mode)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::UInt64(Some(
+                    result as u64,
+                ))))
+            }
+
+            // Float scalars
+            ScalarValue::Float16(Some(v)) => {
+                let result = round_float(*v, scale);
+                Ok(ColumnarValue::Scalar(ScalarValue::Float16(Some(result))))
+            }
+            ScalarValue::Float32(Some(v)) => {
+                let result = round_float(*v, scale);
+                Ok(ColumnarValue::Scalar(ScalarValue::Float32(Some(result))))
+            }
+            ScalarValue::Float64(Some(v)) => {
+                let result = round_float(*v, scale);
+                Ok(ColumnarValue::Scalar(ScalarValue::Float64(Some(result))))
+            }
+
+            // Decimal scalars
+            ScalarValue::Decimal32(Some(v), precision, input_scale) => {
+                let rounded = round_decimal(*v, *input_scale, scale)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::Decimal32(
+                    Some(rounded),
+                    *precision,
+                    *input_scale,
+                )))
+            }
+            ScalarValue::Decimal64(Some(v), precision, input_scale) => {
+                let rounded = round_decimal(*v, *input_scale, scale)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::Decimal64(
+                    Some(rounded),
+                    *precision,
+                    *input_scale,
+                )))
+            }
+            ScalarValue::Decimal128(Some(v), precision, input_scale) => {
+                let rounded = round_decimal(*v, *input_scale, scale)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::Decimal128(
+                    Some(rounded),
+                    *precision,
+                    *input_scale,
+                )))
+            }
+            ScalarValue::Decimal256(Some(v), precision, input_scale) => {
+                let rounded = round_decimal(*v, *input_scale, scale)?;
+                Ok(ColumnarValue::Scalar(ScalarValue::Decimal256(
+                    Some(rounded),
+                    *precision,
+                    *input_scale,
+                )))
+            }
+
+            dt => not_impl_err!("Unsupported data type for Spark round(): {dt}"),
+        },
+    }
+}
diff --git a/datafusion/sqllogictest/test_files/spark/math/round.slt b/datafusion/sqllogictest/test_files/spark/math/round.slt
index bc1f6b7224..91c5bdf050 100644
--- a/datafusion/sqllogictest/test_files/spark/math/round.slt
+++ b/datafusion/sqllogictest/test_files/spark/math/round.slt
@@ -15,13 +15,567 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# This file was originally created by a porting script from:
-#   https://github.com/lakehq/sail/tree/43b6ed8221de5c4c4adbedbb267ae1351158b43c/crates/sail-spark-connect/tests/gold_data/function
 # This file is part of the implementation of the datafusion-spark function library.
 # For more information, please see:
 #   https://github.com/apache/datafusion/issues/15914
 
-## Original Query: SELECT round(2.5, 0);
-## PySpark 3.5.5 Result: {'round(2.5, 0)': Decimal('3'), 'typeof(round(2.5, 0))': 'decimal(2,0)', 'typeof(2.5)': 'decimal(2,1)', 'typeof(0)': 'int'}
-#query
-#SELECT round(2.5::decimal(2,1), 0::int);
+# -------------------------------------------------------------------
+# Float / Double tests (HALF_UP rounding: .5 rounds away from zero)
+# -------------------------------------------------------------------
+
+# round(double) default scale = 0
+query R
+SELECT round(2.5::double);
+----
+3
+
+query R
+SELECT round(3.5::double);
+----
+4
+
+query R
+SELECT round(-2.5::double);
+----
+-3
+
+query R
+SELECT round(-3.5::double);
+----
+-4
+
+query R
+SELECT round(1.4::double);
+----
+1
+
+query R
+SELECT round(1.6::double);
+----
+2
+
+# round(double, scale)
+query R
+SELECT round(2.345::double, 2::int);
+----
+2.35
+
+query R
+SELECT round(2.355::double, 2::int);
+----
+2.36
+
+query R
+SELECT round(123.456::double, 1::int);
+----
+123.5
+
+# round(float)
+query R
+SELECT round(arrow_cast(2.5, 'Float32'));
+----
+3
+
+query R
+SELECT round(arrow_cast(-2.5, 'Float32'));
+----
+-3
+
+# round(float) with scale
+query R
+SELECT round(arrow_cast(2.345, 'Float32'), 2::int);
+----
+2.35
+
+# round(float32) with negative scale
+query R
+SELECT round(arrow_cast(125.0, 'Float32'), -1::int);
+----
+130
+
+# -------------------------------------------------------------------
+# Float16 tests
+# -------------------------------------------------------------------
+
+# round(float16) default scale = 0
+query R
+SELECT round(arrow_cast(2.5, 'Float16'));
+----
+3
+
+query R
+SELECT round(arrow_cast(-2.5, 'Float16'));
+----
+-3
+
+# round(float16) with negative scale
+query R
+SELECT round(arrow_cast(125, 'Float16'), -1::int);
+----
+130
+
+# round(double) with negative scale
+query R
+SELECT round(125.0::double, -1::int);
+----
+130
+
+query R
+SELECT round(125.0::double, -2::int);
+----
+100
+
+query R
+SELECT round(0.0::double);
+----
+0
+
+# round(double) with Infinity and NaN
+query R
+SELECT round('Infinity'::double);
+----
+Infinity
+
+query R
+SELECT round('-Infinity'::double);
+----
+-Infinity
+
+query R
+SELECT round('NaN'::double);
+----
+NaN
+
+query R
+SELECT round('Infinity'::double, 2::int);
+----
+Infinity
+
+query R
+SELECT round('NaN'::double, 2::int);
+----
+NaN
+
+# round(double) with extreme negative scale — should return 0, not NaN
+query R
+SELECT round(42.0::double, -400::int);
+----
+0
+
+# round(double) with extreme positive scale — should return value as-is
+query R
+SELECT round(2.5::double, 400::int);
+----
+2.5
+
+# -------------------------------------------------------------------
+# Integer tests (negative scale rounds to tens, hundreds, etc.)
+# -------------------------------------------------------------------
+
+# round(int, -1) — round to nearest 10
+query I
+SELECT round(25::int, -1::int);
+----
+30
+
+query I
+SELECT round(24::int, -1::int);
+----
+20
+
+query I
+SELECT round(-25::int, -1::int);
+----
+-30
+
+query I
+SELECT round(123::int, -1::int);
+----
+120
+
+# round(int, -2) — round to nearest 100
+query I
+SELECT round(150::int, -2::int);
+----
+200
+
+query I
+SELECT round(149::int, -2::int);
+----
+100
+
+# round(int, positive scale) — no-op for integers
+query I
+SELECT round(42::int, 2::int);
+----
+42
+
+# round(int) default scale = 0 — returns unchanged
+query I
+SELECT round(42::int);
+----
+42
+
+# round(bigint, -1)
+query I
+SELECT round(25::bigint, -1::int);
+----
+30
+
+# round(smallint, -1)
+query I
+SELECT round(25::smallint, -1::int);
+----
+30
+
+# round(tinyint, -1)
+query I
+SELECT round(arrow_cast(25, 'Int8'), -1::int);
+----
+30
+
+# round(int) with very large negative scale — should return 0
+query I
+SELECT round(42::int, -10::int);
+----
+0
+
+# -------------------------------------------------------------------
+# Unsigned integer tests
+# -------------------------------------------------------------------
+
+# round(uint8, -1)
+query I
+SELECT round(arrow_cast(25, 'UInt8'), -1::int);
+----
+30
+
+# round(uint16, -1)
+query I
+SELECT round(arrow_cast(25, 'UInt16'), -1::int);
+----
+30
+
+# round(uint32, -1)
+query I
+SELECT round(arrow_cast(150, 'UInt32'), -2::int);
+----
+200
+
+# round(uint64, -1)
+query I
+SELECT round(arrow_cast(25, 'UInt64'), -1::int);
+----
+30
+
+# round(uint32, positive scale) — no-op for integers
+query I
+SELECT round(arrow_cast(42, 'UInt32'), 2::int);
+----
+42
+
+# -------------------------------------------------------------------
+# Decimal tests (HALF_UP rounding)
+# -------------------------------------------------------------------
+
+# --- Decimal32 ---
+
+# round(decimal32, 0) — round to integer
+query ?
+SELECT round(arrow_cast(2.5, 'Decimal32(9, 1)'), 0::int);
+----
+3.0
+
+query ?
+SELECT round(arrow_cast(-2.5, 'Decimal32(9, 1)'), 0::int);
+----
+-3.0
+
+# round(decimal32, 2)
+query ?
+SELECT round(arrow_cast(2.345, 'Decimal32(9, 3)'), 2::int);
+----
+2.350
+
+# round(decimal32) default scale = 0
+query ?
+SELECT round(arrow_cast(3.5, 'Decimal32(9, 1)'));
+----
+4.0
+
+# --- Decimal64 ---
+
+# round(decimal64, 0) — round to integer
+query ?
+SELECT round(arrow_cast(2.5, 'Decimal64(18, 1)'), 0::int);
+----
+3.0
+
+query ?
+SELECT round(arrow_cast(-2.5, 'Decimal64(18, 1)'), 0::int);
+----
+-3.0
+
+# round(decimal64, 2)
+query ?
+SELECT round(arrow_cast(2.345, 'Decimal64(18, 3)'), 2::int);
+----
+2.350
+
+# round(decimal64) default scale = 0
+query ?
+SELECT round(arrow_cast(3.5, 'Decimal64(18, 1)'));
+----
+4.0
+
+# --- Decimal128 ---
+
+# round(decimal, 0) — round to integer
+query R
+SELECT round(2.5::decimal(2,1), 0::int);
+----
+3
+
+query R
+SELECT round(3.5::decimal(2,1), 0::int);
+----
+4
+
+query R
+SELECT round(-2.5::decimal(2,1), 0::int);
+----
+-3
+
+# round(decimal) default scale = 0
+query R
+SELECT round(2.5::decimal(2,1));
+----
+3
+
+# round(decimal, 2) — keep 2 decimal places
+query R
+SELECT round(2.345::decimal(10,3), 2::int);
+----
+2.35
+
+query R
+SELECT round(2.355::decimal(10,3), 2::int);
+----
+2.36
+
+# round(decimal, scale larger than input scale) — no change
+query R
+SELECT round(2.5::decimal(2,1), 5::int);
+----
+2.5
+
+# round(decimal, 1)
+query R
+SELECT round(123.456::decimal(10,3), 1::int);
+----
+123.5
+
+# round(decimal, negative scale) — round to tens
+query R
+SELECT round(125.0::decimal(10,1), -1::int);
+----
+130
+
+# round(decimal, extreme negative scale) — should return 0, not error
+query R
+SELECT round(2.5::decimal(10,1), -400::int);
+----
+0
+
+# --- Decimal256 ---
+
+# round(decimal256, 0) — round to integer
+query R
+SELECT round(arrow_cast(2.5, 'Decimal256(38, 1)'), 0::int);
+----
+3
+
+query R
+SELECT round(arrow_cast(-2.5, 'Decimal256(38, 1)'), 0::int);
+----
+-3
+
+# round(decimal256, 2)
+query R
+SELECT round(arrow_cast(2.345, 'Decimal256(38, 3)'), 2::int);
+----
+2.35
+
+# round(decimal256) default scale = 0
+query R
+SELECT round(arrow_cast(3.5, 'Decimal256(38, 1)'));
+----
+4
+
+# -------------------------------------------------------------------
+# NULL handling
+# -------------------------------------------------------------------
+
+query I
+SELECT round(NULL::int);
+----
+NULL
+
+query R
+SELECT round(NULL::double);
+----
+NULL
+
+query R
+SELECT round(NULL::decimal(10,2));
+----
+NULL
+
+# round with NULL scale — Spark returns NULL
+query I
+SELECT round(42::int, NULL::int);
+----
+NULL
+
+query R
+SELECT round(2.5::double, NULL::int);
+----
+NULL
+
+# -------------------------------------------------------------------
+# Column-based tests
+# -------------------------------------------------------------------
+
+statement ok
+CREATE TABLE test_round (id int, int_val int, float_val double, dec_val decimal(10,3)) AS VALUES
+  (1, 25, 2.5, 2.345),
+  (2, 35, 3.5, 3.555),
+  (3, -25, -2.5, -2.345),
+  (4, 123, 1.4, 1.005),
+  (5, NULL, NULL, NULL);
+
+query IIRR rowsort
+SELECT id, round(int_val, -1::int), round(float_val), round(dec_val, 2::int) FROM test_round;
+----
+1 30 3 2.35
+2 40 4 3.56
+3 -30 -3 -2.35
+4 120 1 1.01
+5 NULL NULL NULL
+
+statement ok
+DROP TABLE test_round;
+
+# -------------------------------------------------------------------
+# Expression tests
+# -------------------------------------------------------------------
+
+query R
+SELECT round(3.14159::double, 2::int) + 1.0;
+----
+4.140000000000001
+
+# -------------------------------------------------------------------
+# Non-ANSI wrapping behavior
+# When ANSI mode is off, integer overflow wraps silently.
+# -------------------------------------------------------------------
+
+# round(127::tinyint, -1) → 130, wraps as i8 → -126
+query I
+SELECT round(arrow_cast(127, 'Int8'), -1::int);
+----
+-126
+
+# round(32767::smallint, -1) → 32770, wraps as i16 → -32766
+query I
+SELECT round(arrow_cast(32767, 'Int16'), -1::int);
+----
+-32766
+
+# round(2147483647::int, -1) → 2147483650, wraps as i32 → -2147483646
+query I
+SELECT round(2147483647::int, -1::int);
+----
+-2147483646
+
+# round(i64::MAX, -1) wraps as i64 → -9223372036854775806
+query I
+SELECT round(9223372036854775807::bigint, -1::int);
+----
+-9223372036854775806
+
+# -------------------------------------------------------------------
+# ANSI mode tests: overflow detection for integer rounding
+# -------------------------------------------------------------------
+
+statement ok
+set datafusion.execution.enable_ansi_mode = true;
+
+# ANSI mode: normal rounding should still work
+query I
+SELECT round(25::int, -1::int);
+----
+30
+
+query I
+SELECT round(-25::int, -1::int);
+----
+-30
+
+query I
+SELECT round(150::int, -2::int);
+----
+200
+
+# ANSI mode: positive scale on integers — no-op, no overflow
+query I
+SELECT round(42::int, 2::int);
+----
+42
+
+# ANSI mode: floats and decimals should work normally
+query R
+SELECT round(2.5::double);
+----
+3
+
+query R
+SELECT round(2.5::decimal(2,1), 0::int);
+----
+3
+
+# ANSI mode: integer overflow should error
+query error DataFusion error: Execution error: Int64 overflow on round
+SELECT round(9223372036854775807::bigint, -1::int);
+
+# ANSI mode: Int32 overflow should error
+query error DataFusion error: Execution error: Int32 overflow on round
+SELECT round(2147483647::int, -1::int);
+
+# ANSI mode: Int16 overflow should error
+query error DataFusion error: Execution error: Int16 overflow on round
+SELECT round(arrow_cast(32767, 'Int16'), -1::int);
+
+# ANSI mode: Int8 overflow should error
+query error DataFusion error: Execution error: Int8 overflow on round
+SELECT round(arrow_cast(127, 'Int8'), -1::int);
+
+# Reset ANSI mode
+statement ok
+set datafusion.execution.enable_ansi_mode = false;
+
+# -------------------------------------------------------------------
+# Negative tests: unsupported data types
+# -------------------------------------------------------------------
+
+# round(string) should fail
+query error Error during planning: Internal error: Function 'round' failed to match any signature
+SELECT round('hello'::varchar);
+
+# round(boolean) should fail
+query error Error during planning: Internal error: Function 'round' failed to match any signature
+SELECT round(true);
+
+# round(timestamp) should fail
+query error Error during planning: Internal error: Function 'round' failed to match any signature
+SELECT round('2023-01-01T00:00:00'::timestamp);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org