You are viewing a plain text version of this content. The canonical link for it is here.
Posted to github@datafusion.apache.org by "andygrove (via GitHub)" <gi...@apache.org> on 2026/03/02 17:53:41 UTC

[PR] feat: fused WideDecimalBinaryExpr for Decimal128 add/sub/mul [datafusion-comet]

andygrove opened a new pull request, #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619

   ## Summary
   
   - Replace the 4-node expression tree (`Cast(Decimal256→Decimal128, BinaryExpr(op, Cast(Decimal128→Decimal256, left), Cast(Decimal128→Decimal256, right)))`) used for Decimal128 add/sub/mul that may overflow with a single fused `WideDecimalBinaryExpr` that performs i256 register arithmetic directly
   - Reduces per-batch allocation from 4 intermediate arrays (3 Decimal256 @ 32 bytes/elem + 1 Decimal128 @ 16 bytes/elem = 112 bytes/elem) to 1 output array (16 bytes/elem)
   - Add criterion benchmark comparing old vs fused approach
   
   ### Benchmark results (8192 element batches)
   
   | Case | Old | Fused | Speedup |
   |------|-----|-------|---------|
   | add (same scale) | 171 µs | 57 µs | **3.0x** |
   | add (diff scale) | 173 µs | 57 µs | **3.0x** |
   | multiply | 361 µs | 305 µs | **1.2x** |
   | subtract | 173 µs | 58 µs | **3.0x** |
   
   ### How it works
   
   `WideDecimalBinaryExpr` evaluates left/right children, performs add/sub/mul using `i256` intermediates via `arrow::compute::kernels::arity::try_binary`, applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs a single `Decimal128` array. Follows the same pattern as `decimal_div` in `div.rs`.
   
   Overflow handling matches existing behavior:
   - **Ansi mode**: returns `ArrowError::ComputeError`
   - **Legacy/Try mode**: uses `i128::MAX` sentinel + `null_if_overflow_precision`
   
   ## Test plan
   
   - [x] 11 new Rust unit tests (add/sub/mul same/different scales, HALF_UP rounding, overflow in both modes, null propagation, edge cases)
   - [x] `cargo clippy --all-targets --workspace -- -D warnings` passes
   - [x] `cargo test` passes
   - [ ] Existing JVM tests (`CometExpressionSuite`) pass unchanged


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-4014283003

   Moving to draft until I can address https://github.com/apache/datafusion-comet/pull/3619#discussion_r2886822981


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "parthchandra (via GitHub)" <gi...@apache.org>.
parthchandra commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2897212225


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   @andygrove did you miss this comment? 



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] feat: fused WideDecimalBinaryExpr for Decimal128 add/sub/mul [datafusion-comet]

Posted by "sqlbenchmark (via GitHub)" <gi...@apache.org>.
sqlbenchmark commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-3987096938

   Benchmark job `comet-pr-3619-c3986837690` failed due to an error.


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2897967928


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   It is very possible. I will take another look on Monday. I will move to draft for now to avoid accidental merge.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-4008249102

   Thanks for the review @martin-g and @parthchandra. I pushed a commit to address this feedback and also added some additional tests.


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] feat: fused WideDecimalBinaryExpr for Decimal128 add/sub/mul [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-3986837690

   @sqlbenchmark run tpch --iterations 3


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2920888611


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   Good catch! This has been addressed. The code now handles both directions - scaling down when `s_out < max_scale` and scaling up when `s_out > max_scale`. See the `need_scale_down` / `need_scale_up` logic.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2920888611


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   This has been addressed. The code now handles both directions - scaling down when `s_out < max_scale` and scaling up when `s_out > max_scale`. See the `need_scale_down` / `need_scale_up` logic.



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "martin-g (via GitHub)" <gi...@apache.org>.
martin-g commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-4010716888

   Close+reopen to re-trigger CI.


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "martin-g (via GitHub)" <gi...@apache.org>.
martin-g commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2889422328


##########
native/spark-expr/src/math_funcs/internal/decimal_rescale_check.rs:
##########
@@ -0,0 +1,431 @@
+// 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.
+
+//! Fused decimal rescale + overflow check expression.
+//!
+//! Replaces the pattern `CheckOverflow(Cast(expr, Decimal128(p2,s2)), Decimal128(p2,s2))`
+//! with a single expression that rescales and validates precision in one pass.
+
+use arrow::array::{as_primitive_array, Array, ArrayRef, Decimal128Array};
+use arrow::datatypes::{DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::hash::Hash;
+use std::{
+    any::Any,
+    fmt::{Display, Formatter},
+    sync::Arc,
+};
+
+/// A fused expression that rescales a Decimal128 value (changing scale) and checks
+/// for precision overflow in a single pass. Replaces the two-step
+/// `CheckOverflow(Cast(expr, Decimal128(p,s)))` pattern.
+#[derive(Debug, Eq)]
+pub struct DecimalRescaleCheckOverflow {
+    child: Arc<dyn PhysicalExpr>,
+    input_scale: i8,
+    output_precision: u8,
+    output_scale: i8,
+    fail_on_error: bool,
+}
+
+impl Hash for DecimalRescaleCheckOverflow {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.child.hash(state);
+        self.input_scale.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.fail_on_error.hash(state);
+    }
+}
+
+impl PartialEq for DecimalRescaleCheckOverflow {
+    fn eq(&self, other: &Self) -> bool {
+        self.child.eq(&other.child)
+            && self.input_scale == other.input_scale
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.fail_on_error == other.fail_on_error
+    }
+}
+
+impl DecimalRescaleCheckOverflow {
+    pub fn new(
+        child: Arc<dyn PhysicalExpr>,
+        input_scale: i8,
+        output_precision: u8,
+        output_scale: i8,
+        fail_on_error: bool,
+    ) -> Self {
+        Self {
+            child,
+            input_scale,
+            output_precision,
+            output_scale,
+            fail_on_error,
+        }
+    }
+}
+
+impl Display for DecimalRescaleCheckOverflow {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "DecimalRescaleCheckOverflow [child: {}, input_scale: {}, output: Decimal128({}, {}), fail_on_error: {}]",
+            self.child, self.input_scale, self.output_precision, self.output_scale, self.fail_on_error
+        )
+    }
+}
+
+/// Maximum absolute value for a given decimal precision: 10^p - 1.
+#[inline]
+fn precision_bound(precision: u8) -> i128 {
+    10i128.pow(precision as u32) - 1

Review Comment:
   maybe add some validation that `precision` is <=38 ?
   or use checked_pow() and return a Result



##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE
+                };
+
+                arrow::compute::kernels::arity::try_binary(left, right, |l, r| {
+                    let l256 = i256::from_i128(l).wrapping_mul(l_scale_up);
+                    let r256 = i256::from_i128(r).wrapping_mul(r_scale_up);
+                    let raw = match op {
+                        WideDecimalOp::Add => l256.wrapping_add(r256),
+                        WideDecimalOp::Subtract => l256.wrapping_sub(r256),
+                        _ => unreachable!(),
+                    };
+                    let result = if need_rescale {
+                        div_round_half_up(raw, rescale_divisor)
+                    } else {
+                        raw
+                    };
+                    check_overflow_and_convert(result, bound, neg_bound, eval_mode)
+                })?
+            }
+            WideDecimalOp::Multiply => {
+                let natural_scale = s1 + s2;
+                let need_rescale = s_out < natural_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((natural_scale - s_out) as u32)
+                } else {
+                    i256::ONE
+                };
+
+                arrow::compute::kernels::arity::try_binary(left, right, |l, r| {
+                    let raw = i256::from_i128(l).wrapping_mul(i256::from_i128(r));
+                    let result = if need_rescale {
+                        div_round_half_up(raw, rescale_divisor)
+                    } else {
+                        raw
+                    };
+                    check_overflow_and_convert(result, bound, neg_bound, eval_mode)
+                })?
+            }
+        };
+
+        let result = if eval_mode != EvalMode::Ansi {
+            result.null_if_overflow_precision(p_out)
+        } else {
+            result
+        };
+        let result = result.with_data_type(DataType::Decimal128(p_out, s_out));
+        Ok(ColumnarValue::Array(Arc::new(result)))
+    }
+
+    fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
+        vec![&self.left, &self.right]
+    }
+
+    fn with_new_children(
+        self: Arc<Self>,
+        children: Vec<Arc<dyn PhysicalExpr>>,
+    ) -> Result<Arc<dyn PhysicalExpr>> {

Review Comment:
   ```suggestion
       ) -> Result<Arc<dyn PhysicalExpr>> {
           if children.len() != 2 {
               return Err(DataFusionError::Internal(format!(
                   "WideDecimalBinaryExpr expects 2 children, got {}",
                   children.len()
               )));
           }
   ```



##########
native/core/src/execution/planner.rs:
##########
@@ -376,10 +377,37 @@ impl PhysicalPlanner {
                 )))
             }
             ExprStruct::CheckOverflow(expr) => {
-                let child = self.create_expr(expr.child.as_ref().unwrap(), input_schema)?;
+                let child =
+                    self.create_expr(expr.child.as_ref().unwrap(), Arc::clone(&input_schema))?;
                 let data_type = to_arrow_datatype(expr.datatype.as_ref().unwrap());
                 let fail_on_error = expr.fail_on_error;
 
+                // WideDecimalBinaryExpr already handles overflow — skip redundant check
+                if child
+                    .as_any()
+                    .downcast_ref::<WideDecimalBinaryExpr>()
+                    .is_some()
+                {
+                    return Ok(child);
+                }
+
+                // Fuse Cast(Decimal128→Decimal128) + CheckOverflow into single rescale+check
+                if let Some(cast) = child.as_any().downcast_ref::<Cast>() {

Review Comment:
   Should this also check that the Cast's precision/scale match the output's p/s before fusing ?



##########
native/core/src/execution/planner.rs:
##########
@@ -674,31 +702,31 @@ impl PhysicalPlanner {
         ) {
             (
                 DataFusionOperator::Plus | DataFusionOperator::Minus | DataFusionOperator::Multiply,
-                Ok(DataType::Decimal128(p1, s1)),
-                Ok(DataType::Decimal128(p2, s2)),
+                Ok(DataType::Decimal128(_p1, _s1)),

Review Comment:
   What is the reason to use `_` prefix ? The variables are used



##########
native/spark-expr/src/math_funcs/internal/decimal_rescale_check.rs:
##########
@@ -0,0 +1,431 @@
+// 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.
+
+//! Fused decimal rescale + overflow check expression.
+//!
+//! Replaces the pattern `CheckOverflow(Cast(expr, Decimal128(p2,s2)), Decimal128(p2,s2))`
+//! with a single expression that rescales and validates precision in one pass.
+
+use arrow::array::{as_primitive_array, Array, ArrayRef, Decimal128Array};
+use arrow::datatypes::{DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::hash::Hash;
+use std::{
+    any::Any,
+    fmt::{Display, Formatter},
+    sync::Arc,
+};
+
+/// A fused expression that rescales a Decimal128 value (changing scale) and checks
+/// for precision overflow in a single pass. Replaces the two-step
+/// `CheckOverflow(Cast(expr, Decimal128(p,s)))` pattern.
+#[derive(Debug, Eq)]
+pub struct DecimalRescaleCheckOverflow {
+    child: Arc<dyn PhysicalExpr>,
+    input_scale: i8,
+    output_precision: u8,
+    output_scale: i8,
+    fail_on_error: bool,
+}
+
+impl Hash for DecimalRescaleCheckOverflow {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.child.hash(state);
+        self.input_scale.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.fail_on_error.hash(state);
+    }
+}
+
+impl PartialEq for DecimalRescaleCheckOverflow {
+    fn eq(&self, other: &Self) -> bool {
+        self.child.eq(&other.child)
+            && self.input_scale == other.input_scale
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.fail_on_error == other.fail_on_error
+    }
+}
+
+impl DecimalRescaleCheckOverflow {
+    pub fn new(
+        child: Arc<dyn PhysicalExpr>,
+        input_scale: i8,
+        output_precision: u8,
+        output_scale: i8,
+        fail_on_error: bool,
+    ) -> Self {
+        Self {
+            child,
+            input_scale,
+            output_precision,
+            output_scale,
+            fail_on_error,
+        }
+    }
+}
+
+impl Display for DecimalRescaleCheckOverflow {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "DecimalRescaleCheckOverflow [child: {}, input_scale: {}, output: Decimal128({}, {}), fail_on_error: {}]",
+            self.child, self.input_scale, self.output_precision, self.output_scale, self.fail_on_error
+        )
+    }
+}
+
+/// Maximum absolute value for a given decimal precision: 10^p - 1.
+#[inline]
+fn precision_bound(precision: u8) -> i128 {
+    10i128.pow(precision as u32) - 1
+}
+
+/// Rescale a single i128 value by the given delta (output_scale - input_scale)
+/// and check precision bounds. Returns `Ok(value)` or `Ok(i128::MAX)` as sentinel
+/// for overflow in legacy mode, or `Err` in ANSI mode.
+#[inline]
+fn rescale_and_check(
+    value: i128,
+    delta: i8,
+    scale_factor: i128,
+    bound: i128,
+    fail_on_error: bool,
+) -> Result<i128, ArrowError> {
+    let rescaled = if delta > 0 {
+        // Scale up: multiply. Check for overflow.
+        match value.checked_mul(scale_factor) {
+            Some(v) => v,
+            None => {
+                if fail_on_error {
+                    return Err(ArrowError::ComputeError(
+                        "Decimal overflow during rescale".to_string(),
+                    ));
+                }
+                return Ok(i128::MAX); // sentinel
+            }
+        }
+    } else if delta < 0 {
+        // Scale down with HALF_UP rounding
+        // divisor = 10^(-delta), half = divisor / 2
+        let divisor = scale_factor; // already 10^abs(delta)
+        let half = divisor / 2;
+        let sign = if value < 0 { -1i128 } else { 1i128 };
+        (value + sign * half) / divisor
+    } else {
+        value
+    };
+
+    // Precision check
+    if rescaled.abs() > bound {
+        if fail_on_error {
+            return Err(ArrowError::ComputeError(
+                "Decimal overflow: value does not fit in precision".to_string(),
+            ));
+        }
+        Ok(i128::MAX) // sentinel for null_if_overflow_precision
+    } else {
+        Ok(rescaled)
+    }
+}
+
+impl PhysicalExpr for DecimalRescaleCheckOverflow {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(self, f)
+    }
+
+    fn data_type(&self, _: &Schema) -> datafusion::common::Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _: &Schema) -> datafusion::common::Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> datafusion::common::Result<ColumnarValue> {
+        let arg = self.child.evaluate(batch)?;
+        let delta = self.output_scale - self.input_scale;

Review Comment:
   The scales could be negative.
   Let's say `output_scale=38` ( the maximum supported one) and `input_scale=-1`, the `delta=39`.
   This will lead to an error below at `10i128.pow(abs_delta as u32);`



##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {

Review Comment:
   extreme case: `exp>77` will overflow



##########
native/spark-expr/src/math_funcs/internal/decimal_rescale_check.rs:
##########
@@ -0,0 +1,431 @@
+// 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.
+
+//! Fused decimal rescale + overflow check expression.
+//!
+//! Replaces the pattern `CheckOverflow(Cast(expr, Decimal128(p2,s2)), Decimal128(p2,s2))`
+//! with a single expression that rescales and validates precision in one pass.
+
+use arrow::array::{as_primitive_array, Array, ArrayRef, Decimal128Array};
+use arrow::datatypes::{DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::hash::Hash;
+use std::{
+    any::Any,
+    fmt::{Display, Formatter},
+    sync::Arc,
+};
+
+/// A fused expression that rescales a Decimal128 value (changing scale) and checks
+/// for precision overflow in a single pass. Replaces the two-step
+/// `CheckOverflow(Cast(expr, Decimal128(p,s)))` pattern.
+#[derive(Debug, Eq)]
+pub struct DecimalRescaleCheckOverflow {
+    child: Arc<dyn PhysicalExpr>,
+    input_scale: i8,
+    output_precision: u8,
+    output_scale: i8,
+    fail_on_error: bool,
+}
+
+impl Hash for DecimalRescaleCheckOverflow {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.child.hash(state);
+        self.input_scale.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.fail_on_error.hash(state);
+    }
+}
+
+impl PartialEq for DecimalRescaleCheckOverflow {
+    fn eq(&self, other: &Self) -> bool {
+        self.child.eq(&other.child)
+            && self.input_scale == other.input_scale
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.fail_on_error == other.fail_on_error
+    }
+}
+
+impl DecimalRescaleCheckOverflow {
+    pub fn new(
+        child: Arc<dyn PhysicalExpr>,
+        input_scale: i8,
+        output_precision: u8,
+        output_scale: i8,
+        fail_on_error: bool,
+    ) -> Self {
+        Self {
+            child,
+            input_scale,
+            output_precision,
+            output_scale,
+            fail_on_error,
+        }
+    }
+}
+
+impl Display for DecimalRescaleCheckOverflow {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "DecimalRescaleCheckOverflow [child: {}, input_scale: {}, output: Decimal128({}, {}), fail_on_error: {}]",
+            self.child, self.input_scale, self.output_precision, self.output_scale, self.fail_on_error
+        )
+    }
+}
+
+/// Maximum absolute value for a given decimal precision: 10^p - 1.
+#[inline]
+fn precision_bound(precision: u8) -> i128 {
+    10i128.pow(precision as u32) - 1
+}
+
+/// Rescale a single i128 value by the given delta (output_scale - input_scale)
+/// and check precision bounds. Returns `Ok(value)` or `Ok(i128::MAX)` as sentinel
+/// for overflow in legacy mode, or `Err` in ANSI mode.
+#[inline]
+fn rescale_and_check(
+    value: i128,
+    delta: i8,
+    scale_factor: i128,
+    bound: i128,
+    fail_on_error: bool,
+) -> Result<i128, ArrowError> {
+    let rescaled = if delta > 0 {
+        // Scale up: multiply. Check for overflow.
+        match value.checked_mul(scale_factor) {
+            Some(v) => v,
+            None => {
+                if fail_on_error {
+                    return Err(ArrowError::ComputeError(
+                        "Decimal overflow during rescale".to_string(),
+                    ));
+                }
+                return Ok(i128::MAX); // sentinel
+            }
+        }
+    } else if delta < 0 {
+        // Scale down with HALF_UP rounding
+        // divisor = 10^(-delta), half = divisor / 2
+        let divisor = scale_factor; // already 10^abs(delta)
+        let half = divisor / 2;
+        let sign = if value < 0 { -1i128 } else { 1i128 };
+        (value + sign * half) / divisor
+    } else {
+        value
+    };
+
+    // Precision check
+    if rescaled.abs() > bound {
+        if fail_on_error {
+            return Err(ArrowError::ComputeError(
+                "Decimal overflow: value does not fit in precision".to_string(),
+            ));
+        }
+        Ok(i128::MAX) // sentinel for null_if_overflow_precision
+    } else {
+        Ok(rescaled)
+    }
+}
+
+impl PhysicalExpr for DecimalRescaleCheckOverflow {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(self, f)
+    }
+
+    fn data_type(&self, _: &Schema) -> datafusion::common::Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _: &Schema) -> datafusion::common::Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> datafusion::common::Result<ColumnarValue> {
+        let arg = self.child.evaluate(batch)?;
+        let delta = self.output_scale - self.input_scale;
+        let abs_delta = delta.unsigned_abs();
+        let scale_factor = 10i128.pow(abs_delta as u32);
+        let bound = precision_bound(self.output_precision);
+        let fail_on_error = self.fail_on_error;
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+
+        match arg {
+            ColumnarValue::Array(array)
+                if matches!(array.data_type(), DataType::Decimal128(_, _)) =>
+            {
+                let decimal_array = as_primitive_array::<Decimal128Type>(&array);
+
+                let result: Decimal128Array =
+                    arrow::compute::kernels::arity::try_unary(decimal_array, |value| {
+                        rescale_and_check(value, delta, scale_factor, bound, fail_on_error)
+                    })?;
+
+                let result = if !fail_on_error {
+                    result.null_if_overflow_precision(p_out)
+                } else {
+                    result
+                };
+
+                let result = result
+                    .with_precision_and_scale(p_out, s_out)
+                    .map(|a| Arc::new(a) as ArrayRef)?;
+
+                Ok(ColumnarValue::Array(result))
+            }
+            ColumnarValue::Scalar(ScalarValue::Decimal128(v, _precision, _scale)) => {
+                let new_v = v.and_then(|val| {
+                    rescale_and_check(val, delta, scale_factor, bound, fail_on_error)
+                        .ok()

Review Comment:
   If `fail_on_error=true` then this `.ok()` will convert it to `None` and hide the error



##########
native/spark-expr/src/math_funcs/internal/decimal_rescale_check.rs:
##########
@@ -0,0 +1,431 @@
+// 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.
+
+//! Fused decimal rescale + overflow check expression.
+//!
+//! Replaces the pattern `CheckOverflow(Cast(expr, Decimal128(p2,s2)), Decimal128(p2,s2))`
+//! with a single expression that rescales and validates precision in one pass.
+
+use arrow::array::{as_primitive_array, Array, ArrayRef, Decimal128Array};
+use arrow::datatypes::{DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::hash::Hash;
+use std::{
+    any::Any,
+    fmt::{Display, Formatter},
+    sync::Arc,
+};
+
+/// A fused expression that rescales a Decimal128 value (changing scale) and checks
+/// for precision overflow in a single pass. Replaces the two-step
+/// `CheckOverflow(Cast(expr, Decimal128(p,s)))` pattern.
+#[derive(Debug, Eq)]
+pub struct DecimalRescaleCheckOverflow {
+    child: Arc<dyn PhysicalExpr>,
+    input_scale: i8,
+    output_precision: u8,
+    output_scale: i8,
+    fail_on_error: bool,
+}
+
+impl Hash for DecimalRescaleCheckOverflow {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.child.hash(state);
+        self.input_scale.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.fail_on_error.hash(state);
+    }
+}
+
+impl PartialEq for DecimalRescaleCheckOverflow {
+    fn eq(&self, other: &Self) -> bool {
+        self.child.eq(&other.child)
+            && self.input_scale == other.input_scale
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.fail_on_error == other.fail_on_error
+    }
+}
+
+impl DecimalRescaleCheckOverflow {
+    pub fn new(
+        child: Arc<dyn PhysicalExpr>,
+        input_scale: i8,
+        output_precision: u8,
+        output_scale: i8,
+        fail_on_error: bool,
+    ) -> Self {
+        Self {
+            child,
+            input_scale,
+            output_precision,
+            output_scale,
+            fail_on_error,
+        }
+    }
+}
+
+impl Display for DecimalRescaleCheckOverflow {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "DecimalRescaleCheckOverflow [child: {}, input_scale: {}, output: Decimal128({}, {}), fail_on_error: {}]",
+            self.child, self.input_scale, self.output_precision, self.output_scale, self.fail_on_error
+        )
+    }
+}
+
+/// Maximum absolute value for a given decimal precision: 10^p - 1.
+#[inline]
+fn precision_bound(precision: u8) -> i128 {
+    10i128.pow(precision as u32) - 1
+}
+
+/// Rescale a single i128 value by the given delta (output_scale - input_scale)
+/// and check precision bounds. Returns `Ok(value)` or `Ok(i128::MAX)` as sentinel
+/// for overflow in legacy mode, or `Err` in ANSI mode.
+#[inline]
+fn rescale_and_check(
+    value: i128,
+    delta: i8,
+    scale_factor: i128,
+    bound: i128,
+    fail_on_error: bool,
+) -> Result<i128, ArrowError> {
+    let rescaled = if delta > 0 {
+        // Scale up: multiply. Check for overflow.
+        match value.checked_mul(scale_factor) {
+            Some(v) => v,
+            None => {
+                if fail_on_error {
+                    return Err(ArrowError::ComputeError(
+                        "Decimal overflow during rescale".to_string(),
+                    ));
+                }
+                return Ok(i128::MAX); // sentinel
+            }
+        }
+    } else if delta < 0 {
+        // Scale down with HALF_UP rounding
+        // divisor = 10^(-delta), half = divisor / 2
+        let divisor = scale_factor; // already 10^abs(delta)
+        let half = divisor / 2;
+        let sign = if value < 0 { -1i128 } else { 1i128 };
+        (value + sign * half) / divisor
+    } else {
+        value
+    };
+
+    // Precision check
+    if rescaled.abs() > bound {
+        if fail_on_error {
+            return Err(ArrowError::ComputeError(
+                "Decimal overflow: value does not fit in precision".to_string(),
+            ));
+        }
+        Ok(i128::MAX) // sentinel for null_if_overflow_precision
+    } else {
+        Ok(rescaled)
+    }
+}
+
+impl PhysicalExpr for DecimalRescaleCheckOverflow {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Display::fmt(self, f)
+    }
+
+    fn data_type(&self, _: &Schema) -> datafusion::common::Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _: &Schema) -> datafusion::common::Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> datafusion::common::Result<ColumnarValue> {
+        let arg = self.child.evaluate(batch)?;
+        let delta = self.output_scale - self.input_scale;
+        let abs_delta = delta.unsigned_abs();
+        let scale_factor = 10i128.pow(abs_delta as u32);
+        let bound = precision_bound(self.output_precision);
+        let fail_on_error = self.fail_on_error;
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+
+        match arg {
+            ColumnarValue::Array(array)
+                if matches!(array.data_type(), DataType::Decimal128(_, _)) =>
+            {
+                let decimal_array = as_primitive_array::<Decimal128Type>(&array);
+
+                let result: Decimal128Array =
+                    arrow::compute::kernels::arity::try_unary(decimal_array, |value| {
+                        rescale_and_check(value, delta, scale_factor, bound, fail_on_error)
+                    })?;
+
+                let result = if !fail_on_error {
+                    result.null_if_overflow_precision(p_out)
+                } else {
+                    result
+                };
+
+                let result = result
+                    .with_precision_and_scale(p_out, s_out)
+                    .map(|a| Arc::new(a) as ArrayRef)?;
+
+                Ok(ColumnarValue::Array(result))
+            }
+            ColumnarValue::Scalar(ScalarValue::Decimal128(v, _precision, _scale)) => {
+                let new_v = v.and_then(|val| {
+                    rescale_and_check(val, delta, scale_factor, bound, fail_on_error)
+                        .ok()
+                        .and_then(|r| if r == i128::MAX { None } else { Some(r) })
+                });
+                Ok(ColumnarValue::Scalar(ScalarValue::Decimal128(
+                    new_v, p_out, s_out,
+                )))
+            }
+            v => Err(DataFusionError::Execution(format!(
+                "DecimalRescaleCheckOverflow expects Decimal128, but found {v:?}"
+            ))),
+        }
+    }
+
+    fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
+        vec![&self.child]
+    }
+
+    fn with_new_children(
+        self: Arc<Self>,
+        children: Vec<Arc<dyn PhysicalExpr>>,
+    ) -> datafusion::common::Result<Arc<dyn PhysicalExpr>> {

Review Comment:
   ```suggestion
       ) -> datafusion::common::Result<Arc<dyn PhysicalExpr>> {
           if children.len() != 1 {
               return Err(DataFusionError::Internal(format!(
                   "DecimalRescaleCheckOverflow expects 1 child, got {}",
                       children.len()
               )));
           }
   ```
   



##########
native/spark-expr/src/math_funcs/internal/decimal_rescale_check.rs:
##########
@@ -0,0 +1,431 @@
+// 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.
+
+//! Fused decimal rescale + overflow check expression.
+//!
+//! Replaces the pattern `CheckOverflow(Cast(expr, Decimal128(p2,s2)), Decimal128(p2,s2))`
+//! with a single expression that rescales and validates precision in one pass.
+
+use arrow::array::{as_primitive_array, Array, ArrayRef, Decimal128Array};
+use arrow::datatypes::{DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, ScalarValue};
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::hash::Hash;
+use std::{
+    any::Any,
+    fmt::{Display, Formatter},
+    sync::Arc,
+};
+
+/// A fused expression that rescales a Decimal128 value (changing scale) and checks
+/// for precision overflow in a single pass. Replaces the two-step
+/// `CheckOverflow(Cast(expr, Decimal128(p,s)))` pattern.
+#[derive(Debug, Eq)]
+pub struct DecimalRescaleCheckOverflow {
+    child: Arc<dyn PhysicalExpr>,
+    input_scale: i8,
+    output_precision: u8,
+    output_scale: i8,
+    fail_on_error: bool,
+}
+
+impl Hash for DecimalRescaleCheckOverflow {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.child.hash(state);
+        self.input_scale.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.fail_on_error.hash(state);
+    }
+}
+
+impl PartialEq for DecimalRescaleCheckOverflow {
+    fn eq(&self, other: &Self) -> bool {
+        self.child.eq(&other.child)
+            && self.input_scale == other.input_scale
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.fail_on_error == other.fail_on_error
+    }
+}
+
+impl DecimalRescaleCheckOverflow {
+    pub fn new(
+        child: Arc<dyn PhysicalExpr>,
+        input_scale: i8,
+        output_precision: u8,
+        output_scale: i8,
+        fail_on_error: bool,
+    ) -> Self {
+        Self {
+            child,
+            input_scale,
+            output_precision,
+            output_scale,
+            fail_on_error,
+        }
+    }
+}
+
+impl Display for DecimalRescaleCheckOverflow {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "DecimalRescaleCheckOverflow [child: {}, input_scale: {}, output: Decimal128({}, {}), fail_on_error: {}]",
+            self.child, self.input_scale, self.output_precision, self.output_scale, self.fail_on_error
+        )
+    }
+}
+
+/// Maximum absolute value for a given decimal precision: 10^p - 1.
+#[inline]
+fn precision_bound(precision: u8) -> i128 {
+    10i128.pow(precision as u32) - 1
+}
+
+/// Rescale a single i128 value by the given delta (output_scale - input_scale)
+/// and check precision bounds. Returns `Ok(value)` or `Ok(i128::MAX)` as sentinel
+/// for overflow in legacy mode, or `Err` in ANSI mode.
+#[inline]
+fn rescale_and_check(
+    value: i128,
+    delta: i8,
+    scale_factor: i128,
+    bound: i128,
+    fail_on_error: bool,
+) -> Result<i128, ArrowError> {
+    let rescaled = if delta > 0 {
+        // Scale up: multiply. Check for overflow.
+        match value.checked_mul(scale_factor) {
+            Some(v) => v,
+            None => {
+                if fail_on_error {
+                    return Err(ArrowError::ComputeError(
+                        "Decimal overflow during rescale".to_string(),
+                    ));
+                }
+                return Ok(i128::MAX); // sentinel
+            }
+        }
+    } else if delta < 0 {
+        // Scale down with HALF_UP rounding
+        // divisor = 10^(-delta), half = divisor / 2
+        let divisor = scale_factor; // already 10^abs(delta)
+        let half = divisor / 2;
+        let sign = if value < 0 { -1i128 } else { 1i128 };

Review Comment:
   ```suggestion
           let sign = value.signum();
   ```



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove merged PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] feat: fused WideDecimalBinaryExpr for Decimal128 add/sub/mul [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-3985968696

   @sqlbenchmark run tpch --iterations 3


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "parthchandra (via GitHub)" <gi...@apache.org>.
parthchandra commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2886822981


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   Don't we need to scale _up_ to `s_out` if `s_out > max_scale`?



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-4046982932

   Thanks @martin-g @parthchandra @mbutrovich 


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "martin-g (via GitHub)" <gi...@apache.org>.
martin-g closed pull request #3619: perf: Optimize some decimal expressions
URL: https://github.com/apache/datafusion-comet/pull/3619


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on code in PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#discussion_r2920900603


##########
native/spark-expr/src/math_funcs/wide_decimal_binary_expr.rs:
##########
@@ -0,0 +1,502 @@
+// 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.
+
+//! Fused wide-decimal binary expression for Decimal128 add/sub/mul that may overflow.
+//!
+//! Instead of building a 4-node expression tree (Cast→BinaryExpr→Cast→Cast), this performs
+//! i256 intermediate arithmetic in a single expression, producing only one output array.
+
+use crate::math_funcs::utils::get_precision_scale;
+use crate::EvalMode;
+use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array};
+use arrow::datatypes::{i256, DataType, Decimal128Type, Schema};
+use arrow::error::ArrowError;
+use arrow::record_batch::RecordBatch;
+use datafusion::common::Result;
+use datafusion::logical_expr::ColumnarValue;
+use datafusion::physical_expr::PhysicalExpr;
+use std::fmt::{Display, Formatter};
+use std::hash::Hash;
+use std::{any::Any, sync::Arc};
+
+/// The arithmetic operation to perform.
+#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
+pub enum WideDecimalOp {
+    Add,
+    Subtract,
+    Multiply,
+}
+
+impl Display for WideDecimalOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            WideDecimalOp::Add => write!(f, "+"),
+            WideDecimalOp::Subtract => write!(f, "-"),
+            WideDecimalOp::Multiply => write!(f, "*"),
+        }
+    }
+}
+
+/// A fused expression that evaluates Decimal128 add/sub/mul using i256 intermediate arithmetic,
+/// applies scale adjustment with HALF_UP rounding, checks precision bounds, and outputs
+/// a single Decimal128 array.
+#[derive(Debug, Eq)]
+pub struct WideDecimalBinaryExpr {
+    left: Arc<dyn PhysicalExpr>,
+    right: Arc<dyn PhysicalExpr>,
+    op: WideDecimalOp,
+    output_precision: u8,
+    output_scale: i8,
+    eval_mode: EvalMode,
+}
+
+impl Hash for WideDecimalBinaryExpr {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.left.hash(state);
+        self.right.hash(state);
+        self.op.hash(state);
+        self.output_precision.hash(state);
+        self.output_scale.hash(state);
+        self.eval_mode.hash(state);
+    }
+}
+
+impl PartialEq for WideDecimalBinaryExpr {
+    fn eq(&self, other: &Self) -> bool {
+        self.left.eq(&other.left)
+            && self.right.eq(&other.right)
+            && self.op == other.op
+            && self.output_precision == other.output_precision
+            && self.output_scale == other.output_scale
+            && self.eval_mode == other.eval_mode
+    }
+}
+
+impl WideDecimalBinaryExpr {
+    pub fn new(
+        left: Arc<dyn PhysicalExpr>,
+        right: Arc<dyn PhysicalExpr>,
+        op: WideDecimalOp,
+        output_precision: u8,
+        output_scale: i8,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            left,
+            right,
+            op,
+            output_precision,
+            output_scale,
+            eval_mode,
+        }
+    }
+}
+
+impl Display for WideDecimalBinaryExpr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "WideDecimalBinaryExpr [{} {} {}, output: Decimal128({}, {})]",
+            self.left, self.op, self.right, self.output_precision, self.output_scale
+        )
+    }
+}
+
+/// Compute `value / divisor` with HALF_UP rounding.
+#[inline]
+fn div_round_half_up(value: i256, divisor: i256) -> i256 {
+    let (quot, rem) = (value / divisor, value % divisor);
+    // HALF_UP: if |remainder| * 2 >= |divisor|, round away from zero
+    let abs_rem_x2 = if rem < i256::ZERO {
+        rem.wrapping_neg()
+    } else {
+        rem
+    }
+    .wrapping_mul(i256::from_i128(2));
+    let abs_divisor = if divisor < i256::ZERO {
+        divisor.wrapping_neg()
+    } else {
+        divisor
+    };
+    if abs_rem_x2 >= abs_divisor {
+        if (value < i256::ZERO) != (divisor < i256::ZERO) {
+            quot.wrapping_sub(i256::ONE)
+        } else {
+            quot.wrapping_add(i256::ONE)
+        }
+    } else {
+        quot
+    }
+}
+
+/// i256 constant for 10.
+const I256_TEN: i256 = i256::from_i128(10);
+
+/// Compute 10^exp as i256.
+#[inline]
+fn i256_pow10(exp: u32) -> i256 {
+    let mut result = i256::ONE;
+    for _ in 0..exp {
+        result = result.wrapping_mul(I256_TEN);
+    }
+    result
+}
+
+/// Maximum i128 value for a given decimal precision (1-indexed).
+/// precision p allows values in [-10^p + 1, 10^p - 1].
+#[inline]
+fn max_for_precision(precision: u8) -> i256 {
+    i256_pow10(precision as u32).wrapping_sub(i256::ONE)
+}
+
+impl PhysicalExpr for WideDecimalBinaryExpr {
+    fn as_any(&self) -> &dyn Any {
+        self
+    }
+
+    fn data_type(&self, _input_schema: &Schema) -> Result<DataType> {
+        Ok(DataType::Decimal128(
+            self.output_precision,
+            self.output_scale,
+        ))
+    }
+
+    fn nullable(&self, _input_schema: &Schema) -> Result<bool> {
+        Ok(true)
+    }
+
+    fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
+        let left_val = self.left.evaluate(batch)?;
+        let right_val = self.right.evaluate(batch)?;
+
+        let (left_arr, right_arr): (ArrayRef, ArrayRef) = match (&left_val, &right_val) {
+            (ColumnarValue::Array(l), ColumnarValue::Array(r)) => (Arc::clone(l), Arc::clone(r)),
+            (ColumnarValue::Scalar(l), ColumnarValue::Array(r)) => {
+                (l.to_array_of_size(r.len())?, Arc::clone(r))
+            }
+            (ColumnarValue::Array(l), ColumnarValue::Scalar(r)) => {
+                (Arc::clone(l), r.to_array_of_size(l.len())?)
+            }
+            (ColumnarValue::Scalar(l), ColumnarValue::Scalar(r)) => (l.to_array()?, r.to_array()?),
+        };
+
+        let left = left_arr.as_primitive::<Decimal128Type>();
+        let right = right_arr.as_primitive::<Decimal128Type>();
+        let (_p1, s1) = get_precision_scale(left.data_type());
+        let (_p2, s2) = get_precision_scale(right.data_type());
+
+        let p_out = self.output_precision;
+        let s_out = self.output_scale;
+        let op = self.op;
+        let eval_mode = self.eval_mode;
+
+        let bound = max_for_precision(p_out);
+        let neg_bound = i256::ZERO.wrapping_sub(bound);
+
+        let result: Decimal128Array = match op {
+            WideDecimalOp::Add | WideDecimalOp::Subtract => {
+                let max_scale = std::cmp::max(s1, s2);
+                let l_scale_up = i256_pow10((max_scale - s1) as u32);
+                let r_scale_up = i256_pow10((max_scale - s2) as u32);
+                let need_rescale = s_out < max_scale;
+                let rescale_divisor = if need_rescale {
+                    i256_pow10((max_scale - s_out) as u32)
+                } else {
+                    i256::ONE

Review Comment:
   This was addressed in commit https://github.com/apache/datafusion-comet/pull/3619/changes/02d6a3bc0764a70a74cc3ffe5e8b0e479e575103



-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] perf: Optimize some decimal expressions [datafusion-comet]

Posted by "coderfender (via GitHub)" <gi...@apache.org>.
coderfender commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-3998409648

   This is awesome ! 


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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


Re: [PR] feat: fused WideDecimalBinaryExpr for Decimal128 add/sub/mul [datafusion-comet]

Posted by "andygrove (via GitHub)" <gi...@apache.org>.
andygrove commented on PR #3619:
URL: https://github.com/apache/datafusion-comet/pull/3619#issuecomment-3986195372

   @sqlbenchmark run tpch --iterations 3


-- 
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: github-unsubscribe@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


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