comphead commented on code in PR #7534:
URL: https://github.com/apache/datafusion-comet/pull/7534#discussion_r4532980591


##########
native/spark-expr/src/math_funcs/utils.rs:
##########
@@ -162,3 +143,486 @@ pub(crate) fn get_precision_scale(data_type: &DataType) 
-> (u8, i8) {
     };
     (*precision, *scale)
 }
+
+/// The rounding direction that distinguishes `ceil` from `floor`.
+///
+/// Implemented by marker types so that each direction is monomorphized into 
the element loop
+/// rather than dispatched for every value.
+pub(crate) trait RoundingMode {
+    const NAME: &'static str;
+
+    fn round_f32(x: f32) -> f32;
+    fn round_f64(x: f64) -> f64;
+    fn div_i64(x: i64, div: i64) -> i64;
+    fn div_i128(x: i128, div: i128) -> i128;

Review Comment:
   `div_ceil` and `div_floor` from `num::integer` are already generic over 
`Integer`, and `f32`/`f64` both have `ceil`/`floor` through `num::Float`. So 
`div_i64`/`div_i128` and `round_f32`/`round_f64` could each be one generic 
method, which halves the trait and both impls. A `const CEIL: bool` parameter 
would also fold away at compile time and remove the marker types. I have not 
compiled either variant.



##########
native/spark-expr/src/math_funcs/utils.rs:
##########
@@ -162,3 +143,486 @@ pub(crate) fn get_precision_scale(data_type: &DataType) 
-> (u8, i8) {
     };
     (*precision, *scale)
 }
+
+/// The rounding direction that distinguishes `ceil` from `floor`.

Review Comment:
   `utils.rs` holds the decimal helpers that `div.rs`, `round.rs`, 
`make_decimal.rs` and `wide_decimal_binary_expr.rs` share. The trait, the 
markers, the kernel and its tests (about 480 lines) are used only by `ceil` and 
`floor`, and so is `dispatch_pow10!`. Since `ceil.rs` and `floor.rs` are now 
thin wrappers, could the shared code live in one `ceil_floor.rs` module that 
exports `spark_ceil` and `spark_floor`, and keep `utils.rs` for the 
cross-function helpers?



##########
native/spark-expr/src/math_funcs/utils.rs:
##########
@@ -162,3 +143,486 @@ pub(crate) fn get_precision_scale(data_type: &DataType) 
-> (u8, i8) {
     };
     (*precision, *scale)
 }
+
+/// The rounding direction that distinguishes `ceil` from `floor`.
+///
+/// Implemented by marker types so that each direction is monomorphized into 
the element loop
+/// rather than dispatched for every value.
+pub(crate) trait RoundingMode {
+    const NAME: &'static str;
+
+    fn round_f32(x: f32) -> f32;
+    fn round_f64(x: f64) -> f64;
+    fn div_i64(x: i64, div: i64) -> i64;
+    fn div_i128(x: i128, div: i128) -> i128;
+}
+
+pub(crate) struct Ceil;
+
+impl RoundingMode for Ceil {
+    const NAME: &'static str = "ceil";
+
+    #[inline]
+    fn round_f32(x: f32) -> f32 {
+        x.ceil()
+    }
+
+    #[inline]
+    fn round_f64(x: f64) -> f64 {
+        x.ceil()
+    }
+
+    #[inline]
+    fn div_i64(x: i64, div: i64) -> i64 {
+        div_ceil(x, div)
+    }
+
+    #[inline]
+    fn div_i128(x: i128, div: i128) -> i128 {
+        div_ceil(x, div)
+    }
+}
+
+pub(crate) struct Floor;
+
+impl RoundingMode for Floor {
+    const NAME: &'static str = "floor";
+
+    #[inline]
+    fn round_f32(x: f32) -> f32 {
+        x.floor()
+    }
+
+    #[inline]
+    fn round_f64(x: f64) -> f64 {
+        x.floor()
+    }
+
+    #[inline]
+    fn div_i64(x: i64, div: i64) -> i64 {
+        div_floor(x, div)
+    }
+
+    #[inline]
+    fn div_i128(x: i128, div: i128) -> i128 {
+        div_floor(x, div)
+    }
+}
+
+/// Shared implementation of Spark's `ceil` and `floor` expressions.
+pub(crate) fn spark_ceil_or_floor<M: RoundingMode>(
+    args: &[ColumnarValue],
+    data_type: &DataType,
+) -> Result<ColumnarValue, DataFusionError> {
+    let value = &args[0];
+    match value {
+        ColumnarValue::Array(array) => match array.data_type() {
+            DataType::Float32 => {
+                let result: Int64Array = arrow::compute::kernels::arity::unary(
+                    array.as_primitive::<Float32Type>(),
+                    |x| M::round_f32(x) as i64,
+                );
+                Ok(ColumnarValue::Array(Arc::new(result)))
+            }
+            DataType::Float64 => {
+                let result: Int64Array = arrow::compute::kernels::arity::unary(
+                    array.as_primitive::<Float64Type>(),
+                    |x| M::round_f64(x) as i64,
+                );
+                Ok(ColumnarValue::Array(Arc::new(result)))
+            }
+            DataType::Int64 => {
+                let result = 
array.as_any().downcast_ref::<Int64Array>().unwrap();
+                Ok(ColumnarValue::Array(Arc::new(result.clone())))
+            }
+            DataType::Decimal128(_, input_scale) if *input_scale > 0 => {
+                let (precision, scale) = get_precision_scale(data_type);
+                dispatch_pow10!(
+                    *input_scale,
+                    EXP => make_decimal_array(array, precision, scale, 
decimal_round_pow10::<M, EXP>),
+                    make_decimal_array(array, precision, scale, 
decimal_round_f::<M>(*input_scale))
+                )
+            }
+            other => Err(DataFusionError::Internal(format!(
+                "Unsupported data type {other:?} for function {}",
+                M::NAME,
+            ))),
+        },
+        ColumnarValue::Scalar(a) => match a {
+            ScalarValue::Float32(a) => 
Ok(ColumnarValue::Scalar(ScalarValue::Int64(
+                a.map(|x| M::round_f32(x) as i64),
+            ))),
+            ScalarValue::Float64(a) => 
Ok(ColumnarValue::Scalar(ScalarValue::Int64(
+                a.map(|x| M::round_f64(x) as i64),
+            ))),
+            ScalarValue::Int64(a) => 
Ok(ColumnarValue::Scalar(ScalarValue::Int64(a.map(|x| x)))),
+            ScalarValue::Decimal128(a, _, input_scale) if *input_scale > 0 => {
+                let f = decimal_round_f::<M>(*input_scale);
+                let (precision, scale) = get_precision_scale(data_type);
+                make_decimal_scalar(a, precision, scale, f)
+            }
+            _ => Err(DataFusionError::Internal(format!(
+                "Unsupported data type {:?} for function {}",
+                value.data_type(),
+                M::NAME,
+            ))),
+        },
+    }
+}
+
+#[inline]
+fn decimal_round_f<M: RoundingMode>(scale: i8) -> impl Fn(i128) -> i128 {
+    let div = 10_i128.pow_wrapping(scale as u32);
+    move |x: i128| M::div_i128(x, div)
+}
+
+/// Rounds an unscaled decimal divided by `10^EXP` in the direction of `M`.
+///
+/// `EXP` is a compile-time constant so that the divisor is folded in and the 
division lowered to a
+/// multiply-and-shift. A 128-bit division is always a libcall, even by a 
constant, so values that
+/// fit in 64 bits take a 64-bit path; unscaled decimals rarely exceed that 
range.
+#[inline]
+fn decimal_round_pow10<M: RoundingMode, const EXP: u32>(x: i128) -> i128 {
+    match i64::try_from(x) {
+        Ok(x) => M::div_i64(x, const { 10_i64.pow(EXP) }) as i128,
+        Err(_) => decimal_round_wide::<M>(x, const { 10_i128.pow(EXP) }),
+    }
+}
+
+/// Kept out of line so that the libcall and its stack frame stay out of the 
loop body of every
+/// [`decimal_round_pow10`] instantiation.
+#[cold]
+#[inline(never)]
+fn decimal_round_wide<M: RoundingMode>(x: i128, div: i128) -> i128 {
+    M::div_i128(x, div)
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::{spark_ceil, spark_floor};
+    use arrow::array::{Float32Array, Float64Array};
+    use datafusion::common::cast::as_int64_array;
+    use datafusion::common::Result;
+
+    type RoundFn = fn(&[ColumnarValue], &DataType) -> Result<ColumnarValue>;
+
+    fn check_int64_array(
+        round: RoundFn,
+        input: ArrayRef,
+        expected: Vec<Option<i64>>,
+    ) -> Result<()> {
+        let data_type = input.data_type().clone();
+        let ColumnarValue::Array(result) = 
round(&[ColumnarValue::Array(input)], &data_type)?
+        else {
+            unreachable!()
+        };
+        assert_eq!(as_int64_array(&result)?, &Int64Array::from(expected));
+        Ok(())
+    }
+
+    fn check_decimal_array(
+        round: RoundFn,
+        input: &[Option<i128>],
+        (precision, scale): (u8, i8),
+        output_precision: u8,
+        expected: Vec<Option<i128>>,
+    ) -> Result<()> {
+        let array =
+            
Decimal128Array::from(input.to_vec()).with_precision_and_scale(precision, 
scale)?;
+        let args = [ColumnarValue::Array(Arc::new(array))];
+        let ColumnarValue::Array(result) =
+            round(&args, &DataType::Decimal128(output_precision, 0))?
+        else {
+            unreachable!()
+        };
+        let expected =
+            
Decimal128Array::from(expected).with_precision_and_scale(output_precision, 0)?;
+        let actual = 
result.as_any().downcast_ref::<Decimal128Array>().unwrap();
+        assert_eq!(actual, &expected);
+        Ok(())
+    }
+
+    fn check_scalar(
+        round: RoundFn,
+        input: ScalarValue,
+        data_type: &DataType,
+        expected: ScalarValue,
+    ) -> Result<()> {
+        let ColumnarValue::Scalar(actual) = 
round(&[ColumnarValue::Scalar(input)], data_type)?
+        else {
+            unreachable!()
+        };
+        assert_eq!(actual, expected);
+        Ok(())
+    }
+
+    #[test]
+    fn test_f32_array() -> Result<()> {
+        let ceil_input = [125.2345, 15.0001, 0.1, -0.9, -1.1, 123.0].map(Some);
+        let floor_input = [125.9345, 15.9999, 0.9, -0.1, -1.999, 
123.0].map(Some);
+        let with_null = |values: [Option<f32>; 6]| {
+            let values: Vec<_> = values.into_iter().chain([None]).collect();
+            Arc::new(Float32Array::from(values)) as ArrayRef
+        };
+        check_int64_array(
+            spark_ceil,
+            with_null(ceil_input),
+            vec![
+                Some(126),
+                Some(16),
+                Some(1),
+                Some(0),
+                Some(-1),
+                Some(123),
+                None,
+            ],
+        )?;
+        check_int64_array(
+            spark_floor,
+            with_null(floor_input),
+            vec![
+                Some(125),
+                Some(15),
+                Some(0),
+                Some(-1),
+                Some(-2),
+                Some(123),
+                None,
+            ],
+        )
+    }
+
+    #[test]
+    fn test_f64_array() -> Result<()> {
+        let ceil_input = [125.2345, 15.0001, 0.1, -0.9, -1.1, 123.0].map(Some);
+        let floor_input = [125.9345, 15.9999, 0.9, -0.1, -1.999, 
123.0].map(Some);
+        let with_null = |values: [Option<f64>; 6]| {
+            let values: Vec<_> = values.into_iter().chain([None]).collect();
+            Arc::new(Float64Array::from(values)) as ArrayRef
+        };
+        check_int64_array(
+            spark_ceil,
+            with_null(ceil_input),
+            vec![
+                Some(126),
+                Some(16),
+                Some(1),
+                Some(0),
+                Some(-1),
+                Some(123),
+                None,
+            ],
+        )?;
+        check_int64_array(
+            spark_floor,
+            with_null(floor_input),
+            vec![
+                Some(125),
+                Some(15),
+                Some(0),
+                Some(-1),
+                Some(-2),
+                Some(123),
+                None,
+            ],
+        )
+    }
+
+    #[test]
+    fn test_i64_array() -> Result<()> {
+        for round in [spark_ceil as RoundFn, spark_floor] {
+            let values = vec![Some(-1), Some(0), Some(1), None];
+            let input = Arc::new(Int64Array::from(values.clone())) as ArrayRef;
+            check_int64_array(round, input, values)?;
+        }
+        Ok(())
+    }
+
+    #[test]
+    fn test_decimal128_array() -> Result<()> {
+        let input = [
+            Some(12345),  // 123.45
+            Some(12500),  // 125.00
+            Some(-12999), // -129.99
+            None,
+        ];
+        check_decimal_array(
+            spark_ceil,
+            &input,
+            (5, 2),
+            4,
+            vec![Some(124), Some(125), Some(-129), None],
+        )?;
+        check_decimal_array(
+            spark_floor,
+            &input,
+            (5, 2),
+            4,
+            vec![Some(123), Some(125), Some(-130), None],
+        )
+    }
+
+    #[test]
+    fn test_decimal128_wide_array() -> Result<()> {
+        // Unscaled values above i64::MAX (~9.22e18) take the wide fallback in 
decimal_round_pow10.
+        // The first value is an exact multiple of 10^6, the other two have a 
remainder.
+        let input = [
+            Some(20_000_000_000_000_000_000_i128),
+            Some(20_000_000_000_000_000_001_i128),
+            Some(-20_000_000_000_000_000_001_i128),
+            None,
+        ];
+        check_decimal_array(
+            spark_ceil,
+            &input,
+            (38, 6),
+            38,
+            vec![
+                Some(20_000_000_000_000_i128),
+                Some(20_000_000_000_001_i128),
+                Some(-20_000_000_000_000_i128),
+                None,
+            ],
+        )?;
+        check_decimal_array(
+            spark_floor,
+            &input,
+            (38, 6),
+            38,
+            vec![
+                Some(20_000_000_000_000_i128),
+                Some(20_000_000_000_000_i128),
+                Some(-20_000_000_000_001_i128),
+                None,
+            ],
+        )
+    }
+
+    #[test]
+    fn test_decimal128_i64_boundary_array() -> Result<()> {
+        // decimal_round_pow10 switches from the i64 fast path to 
decimal_round_wide exactly at
+        // i64::MAX / i64::MIN. Straddling both edges pins that the two paths 
agree: each pair
+        // below differs by one unscaled unit but rounds to the same value, so 
a divergence
+        // between the 64-bit and 128-bit divisions would show up as a 
mismatch here.
+        let input = [
+            Some(i64::MAX as i128),     //  9223372036854.775807 -> fast path
+            Some(i64::MAX as i128 + 1), //  9223372036854.775808 -> wide path
+            Some(i64::MIN as i128),     // -9223372036854.775808 -> fast path
+            Some(i64::MIN as i128 - 1), // -9223372036854.775809 -> wide path
+        ];
+        check_decimal_array(
+            spark_ceil,
+            &input,
+            (38, 6),
+            38,
+            vec![
+                Some(9_223_372_036_855_i128),
+                Some(9_223_372_036_855_i128),
+                Some(-9_223_372_036_854_i128),
+                Some(-9_223_372_036_854_i128),
+            ],
+        )?;
+        check_decimal_array(
+            spark_floor,
+            &input,
+            (38, 6),
+            38,
+            vec![
+                Some(9_223_372_036_854_i128),
+                Some(9_223_372_036_854_i128),
+                Some(-9_223_372_036_855_i128),
+                Some(-9_223_372_036_855_i128),
+            ],
+        )
+    }
+
+    #[test]
+    fn test_decimal128_large_scale_array() -> Result<()> {

Review Comment:
   `ceil.sql` and `floor.sql` only cover `FLOAT`, `DOUBLE` and `DECIMAL(5, 2)`, 
so a `BIGINT` column and a wide decimal column (`DECIMAL(38, 6)`, `DECIMAL(38, 
19)`) with values past `i64::MAX` are never compared with Spark. These Rust 
tests also pass `Decimal128(38, 0)` as the output type for a `(38, 6)` input, 
where Spark plans `(33, 0)` (`p - s + 1`). Could a few such rows be added to 
the two SQL files, read from a table so constant folding does not hide the 
native path? The i64 boundary and scale 19 cases could then be checked there 
against Spark too.



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to