Skip to content

Commit 3a56ba9

Browse files
committed
Match Arrow decimal Add/Sub result typing
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 1345b5d commit 3a56ba9

6 files changed

Lines changed: 170 additions & 143 deletions

File tree

vortex-array/src/compute/conformance/binary_numeric.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ use crate::scalar::DecimalValue;
4949
use crate::scalar::NumericOperator;
5050
use crate::scalar::PrimitiveScalar;
5151
use crate::scalar::Scalar;
52+
use crate::scalar_fn::fns::binary::decimal_add_sub_result_dtype;
5253

5354
fn to_vec_of_scalar(array: &ArrayRef, ctx: &mut ExecutionCtx) -> Vec<Scalar> {
5455
// Not fast, but obviously correct
@@ -299,6 +300,8 @@ fn test_decimal_binary_numeric_with_scalar(
299300
let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
300301

301302
let scalar = Scalar::decimal(value, decimal_dtype, array.dtype().nullability());
303+
let result_decimal_dtype = decimal_add_sub_result_dtype(decimal_dtype);
304+
let result_dtype = DType::Decimal(result_decimal_dtype, array.dtype().nullability());
302305

303306
// Decimal Mul/Div are not yet implemented.
304307
let operators = vec![NumericOperator::Add, NumericOperator::Sub];
@@ -321,7 +324,17 @@ fn test_decimal_binary_numeric_with_scalar(
321324
} else {
322325
(scalar.as_decimal(), x.as_decimal())
323326
};
324-
lhs.checked_binary_numeric(&rhs, operator).map(Scalar::from)
327+
let (Some(lhs), Some(rhs)) = (lhs.decimal_value(), rhs.decimal_value()) else {
328+
return Some(Scalar::null(result_dtype.clone()));
329+
};
330+
let value = match operator {
331+
NumericOperator::Add => lhs.checked_add(&rhs),
332+
NumericOperator::Sub => lhs.checked_sub(&rhs),
333+
NumericOperator::Mul | NumericOperator::Div => unreachable!(),
334+
}?;
335+
value.fits_in_precision(result_decimal_dtype).then(|| {
336+
Scalar::decimal(value, result_decimal_dtype, result_dtype.nullability())
337+
})
325338
})
326339
.collect();
327340

vortex-array/src/expr/transform/coerce.rs

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,14 @@ mod tests {
156156

157157
#[test]
158158
fn decimal_arithmetic_coerces_precision_and_scale() -> VortexResult<()> {
159-
let result_dtype = DType::Decimal(DecimalDType::new(4, 2), NonNullable);
159+
let common_dtype = DType::Decimal(DecimalDType::new(4, 2), NonNullable);
160+
let result_dtype = DType::Decimal(DecimalDType::new(5, 2), NonNullable);
160161
let scope = DType::Struct(
161162
StructFields::new(
162163
["a", "b"].into(),
163164
vec![
164165
DType::Decimal(DecimalDType::new(3, 1), NonNullable),
165-
result_dtype.clone(),
166+
common_dtype,
166167
],
167168
),
168169
NonNullable,
@@ -177,29 +178,6 @@ mod tests {
177178
Ok(())
178179
}
179180

180-
#[test]
181-
fn decimal_arithmetic_coerces_integer_operand() -> VortexResult<()> {
182-
let result_dtype = DType::Decimal(DecimalDType::new(5, 2), NonNullable);
183-
let scope = DType::Struct(
184-
StructFields::new(
185-
["a", "b"].into(),
186-
vec![
187-
DType::Decimal(DecimalDType::new(4, 2), NonNullable),
188-
DType::Primitive(PType::I8, NonNullable),
189-
],
190-
),
191-
NonNullable,
192-
);
193-
let expr = Binary.new_expr(Operator::Add, [col("a"), col("b")]);
194-
195-
let coerced = coerce_expression(expr, &scope)?;
196-
197-
assert!(coerced.child(0).is::<Cast>());
198-
assert!(coerced.child(1).is::<Cast>());
199-
assert_eq!(coerced.return_dtype(&scope)?, result_dtype);
200-
Ok(())
201-
}
202-
203181
#[test]
204182
fn boolean_operators_no_coercion() -> VortexResult<()> {
205183
let scope = DType::Struct(

vortex-array/src/scalar_fn/fns/binary/mod.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use vortex_session::registry::CachedId;
1717
use crate::ArrayRef;
1818
use crate::ExecutionCtx;
1919
use crate::dtype::DType;
20+
use crate::dtype::DecimalDType;
21+
use crate::dtype::MAX_PRECISION;
2022
use crate::dtype::Nullability;
2123
use crate::expr::and;
2224
use crate::expr::expression::Expression;
@@ -48,6 +50,14 @@ use crate::scalar::Scalar;
4850
#[derive(Clone)]
4951
pub struct Binary;
5052

53+
/// Derive the result type for Add/Sub over operands that already share a decimal dtype.
54+
pub(crate) fn decimal_add_sub_result_dtype(input: DecimalDType) -> DecimalDType {
55+
DecimalDType::new(
56+
input.precision().saturating_add(1).min(MAX_PRECISION),
57+
input.scale(),
58+
)
59+
}
60+
5161
impl ScalarFnVTable for Binary {
5262
type Options = Operator;
5363

@@ -118,12 +128,18 @@ impl ScalarFnVTable for Binary {
118128
let rhs = &arg_dtypes[1];
119129

120130
if operator.is_arithmetic() {
121-
// Decimal Mul/Div need rescaling support and are not yet implemented.
122-
let decimal_supported =
123-
lhs.is_decimal() && matches!(operator, Operator::Add | Operator::Sub);
124-
if (lhs.is_primitive() || decimal_supported) && lhs.eq_ignore_nullability(rhs) {
131+
if lhs.is_primitive() && lhs.eq_ignore_nullability(rhs) {
125132
return Ok(lhs.with_nullability(lhs.nullability() | rhs.nullability()));
126133
}
134+
if let DType::Decimal(decimal_dtype, _) = lhs
135+
&& matches!(operator, Operator::Add | Operator::Sub)
136+
&& lhs.eq_ignore_nullability(rhs)
137+
{
138+
return Ok(DType::Decimal(
139+
decimal_add_sub_result_dtype(*decimal_dtype),
140+
lhs.nullability() | rhs.nullability(),
141+
));
142+
}
127143
vortex_bail!(
128144
"incompatible types for arithmetic operation: {} {}",
129145
lhs,

vortex-array/src/scalar_fn/fns/binary/numeric/decimal.rs

Lines changed: 30 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
//! Native execution of the arithmetic operators over decimal arrays.
55
//!
6-
//! Both operands share a logical [`DecimalDType`] (equal precision and scale) and the result
7-
//! keeps that dtype: fixed-point arithmetic at the shared scale. Add and Sub apply directly to
8-
//! the unscaled stored integers and are exact; Mul and Div require rescaling and are not yet
9-
//! implemented.
6+
//! Both operands share a logical [`DecimalDType`] (equal precision and scale). Add and Sub apply
7+
//! directly to the unscaled stored integers and are exact at that shared scale. The result reserves
8+
//! one additional precision digit for a carry, capped at Vortex's maximum decimal precision. Mul
9+
//! and Div require rescaling and are not yet implemented.
1010
//!
1111
//! Lanes execute in a working width chosen so that in-precision inputs cannot spuriously
1212
//! overflow an intermediate value. An operation that overflows the result precision on a valid
@@ -25,6 +25,7 @@ use super::CheckedValues;
2525
use super::check_numeric_errors;
2626
use super::checked_all_lanes;
2727
use super::checked_valid_lanes;
28+
use super::decimal_add_sub_result_dtype;
2829
use crate::ArrayRef;
2930
use crate::ExecutionCtx;
3031
use crate::IntoArray;
@@ -54,10 +55,11 @@ pub(super) fn execute_numeric_decimal(
5455
let DType::Decimal(decimal_dtype, _) = lhs.dtype() else {
5556
vortex_bail!("expected a decimal dtype, got {}", lhs.dtype());
5657
};
57-
let decimal_dtype = *decimal_dtype;
58-
let result_dtype = lhs
59-
.dtype()
60-
.with_nullability(lhs.dtype().nullability() | rhs.dtype().nullability());
58+
let result_decimal_dtype = decimal_add_sub_result_dtype(*decimal_dtype);
59+
let result_dtype = DType::Decimal(
60+
result_decimal_dtype,
61+
lhs.dtype().nullability() | rhs.dtype().nullability(),
62+
);
6163

6264
let lhs = DecimalOperand::try_new(lhs, ctx)?;
6365
let rhs = DecimalOperand::try_new(rhs, ctx)?;
@@ -67,82 +69,61 @@ pub(super) fn execute_numeric_decimal(
6769
let validity = lhs.validity().and(rhs.validity())?;
6870
let valid_rows = validity.execute_mask(len, ctx)?;
6971

70-
let work = working_type(decimal_dtype);
71-
let output = DecimalType::smallest_decimal_value_type(&decimal_dtype);
72-
match (work, output) {
73-
(DecimalType::I8, DecimalType::I8) => execute_decimal_at_widths::<i8, i8>(
72+
match DecimalType::smallest_decimal_value_type(&result_decimal_dtype) {
73+
DecimalType::I8 => execute_decimal_at_widths::<i8, i8>(
7474
&lhs,
7575
&rhs,
7676
op,
77-
decimal_dtype,
77+
result_decimal_dtype,
7878
&result_dtype,
7979
validity,
8080
&valid_rows,
8181
),
82-
(DecimalType::I16, DecimalType::I8) => execute_decimal_at_widths::<i16, i8>(
82+
DecimalType::I16 => execute_decimal_at_widths::<i16, i16>(
8383
&lhs,
8484
&rhs,
8585
op,
86-
decimal_dtype,
86+
result_decimal_dtype,
8787
&result_dtype,
8888
validity,
8989
&valid_rows,
9090
),
91-
(DecimalType::I16, DecimalType::I16) => execute_decimal_at_widths::<i16, i16>(
91+
DecimalType::I32 => execute_decimal_at_widths::<i32, i32>(
9292
&lhs,
9393
&rhs,
9494
op,
95-
decimal_dtype,
95+
result_decimal_dtype,
9696
&result_dtype,
9797
validity,
9898
&valid_rows,
9999
),
100-
(DecimalType::I32, DecimalType::I32) => execute_decimal_at_widths::<i32, i32>(
100+
DecimalType::I64 => execute_decimal_at_widths::<i64, i64>(
101101
&lhs,
102102
&rhs,
103103
op,
104-
decimal_dtype,
104+
result_decimal_dtype,
105105
&result_dtype,
106106
validity,
107107
&valid_rows,
108108
),
109-
(DecimalType::I64, DecimalType::I64) => execute_decimal_at_widths::<i64, i64>(
109+
DecimalType::I128 => execute_decimal_at_widths::<i128, i128>(
110110
&lhs,
111111
&rhs,
112112
op,
113-
decimal_dtype,
113+
result_decimal_dtype,
114114
&result_dtype,
115115
validity,
116116
&valid_rows,
117117
),
118-
(DecimalType::I128, DecimalType::I128) => execute_decimal_at_widths::<i128, i128>(
118+
DecimalType::I256 => execute_decimal_at_widths::<i256, i256>(
119119
&lhs,
120120
&rhs,
121121
op,
122-
decimal_dtype,
122+
result_decimal_dtype,
123123
&result_dtype,
124124
validity,
125125
&valid_rows,
126126
),
127-
(DecimalType::I256, DecimalType::I128) => execute_decimal_at_widths::<i256, i128>(
128-
&lhs,
129-
&rhs,
130-
op,
131-
decimal_dtype,
132-
&result_dtype,
133-
validity,
134-
&valid_rows,
135-
),
136-
(DecimalType::I256, DecimalType::I256) => execute_decimal_at_widths::<i256, i256>(
137-
&lhs,
138-
&rhs,
139-
op,
140-
decimal_dtype,
141-
&result_dtype,
142-
validity,
143-
&valid_rows,
144-
),
145-
_ => vortex_bail!("unsupported decimal working/output width combination: {work}/{output}"),
146127
}
147128
}
148129

@@ -198,33 +179,6 @@ impl DecimalOperand {
198179
}
199180
}
200181

201-
/// Choose the smallest lane width that can represent every sum or difference of two valid inputs.
202-
fn working_type(dtype: DecimalDType) -> DecimalType {
203-
let precision = dtype.precision() as usize;
204-
let max = <i256 as NativeDecimalType>::MAX_BY_PRECISION[precision];
205-
let max_result = max
206-
.checked_add(&max)
207-
.vortex_expect("the sum of two valid decimal values must fit in i256");
208-
smallest_value_type(&DecimalValue::from(max_result))
209-
}
210-
211-
/// The smallest decimal value type that can represent `value`, regardless of its stored width.
212-
fn smallest_value_type(value: &DecimalValue) -> DecimalType {
213-
if value.cast::<i8>().is_some() {
214-
DecimalType::I8
215-
} else if value.cast::<i16>().is_some() {
216-
DecimalType::I16
217-
} else if value.cast::<i32>().is_some() {
218-
DecimalType::I32
219-
} else if value.cast::<i64>().is_some() {
220-
DecimalType::I64
221-
} else if value.cast::<i128>().is_some() {
222-
DecimalType::I128
223-
} else {
224-
DecimalType::I256
225-
}
226-
}
227-
228182
/// Per-execution constants for checked decimal lane operations at working width `W`.
229183
struct DecimalOpPlan<W> {
230184
/// Inclusive stored-value bounds implied by the result precision.
@@ -289,7 +243,7 @@ fn execute_decimal_at_widths<W, O>(
289243
lhs: &DecimalOperand,
290244
rhs: &DecimalOperand,
291245
op: NumericOperator,
292-
decimal_dtype: DecimalDType,
246+
result_decimal_dtype: DecimalDType,
293247
result_dtype: &DType,
294248
validity: Validity,
295249
valid_rows: &Mask,
@@ -303,15 +257,15 @@ where
303257
NumericOperator::Add => execute_decimal_typed::<W, O, DecimalAdd>(
304258
lhs,
305259
rhs,
306-
decimal_dtype,
260+
result_decimal_dtype,
307261
result_dtype,
308262
validity,
309263
valid_rows,
310264
),
311265
NumericOperator::Sub => execute_decimal_typed::<W, O, DecimalSub>(
312266
lhs,
313267
rhs,
314-
decimal_dtype,
268+
result_decimal_dtype,
315269
result_dtype,
316270
validity,
317271
valid_rows,
@@ -326,7 +280,7 @@ where
326280
fn execute_decimal_typed<W, O, Op>(
327281
lhs: &DecimalOperand,
328282
rhs: &DecimalOperand,
329-
decimal_dtype: DecimalDType,
283+
result_decimal_dtype: DecimalDType,
330284
result_dtype: &DType,
331285
validity: Validity,
332286
valid_rows: &Mask,
@@ -338,7 +292,7 @@ where
338292
Op: CheckedDecimalOp,
339293
{
340294
let len = lhs.len();
341-
let plan = DecimalOpPlan::<W>::new(decimal_dtype);
295+
let plan = DecimalOpPlan::<W>::new(result_decimal_dtype);
342296

343297
let checked = match (lhs, rhs) {
344298
(DecimalOperand::Array { values: lhs, .. }, DecimalOperand::Array { values: rhs, .. }) => {
@@ -374,7 +328,7 @@ where
374328
return Ok(ConstantArray::new(
375329
Scalar::decimal(
376330
DecimalValue::from(value),
377-
decimal_dtype,
331+
result_decimal_dtype,
378332
result_dtype.nullability(),
379333
),
380334
len,
@@ -389,7 +343,7 @@ where
389343

390344
Ok(DecimalArray::new(
391345
checked.values,
392-
decimal_dtype,
346+
result_decimal_dtype,
393347
validity.union_nullability(result_dtype.nullability()),
394348
)
395349
.into_array())

vortex-array/src/scalar_fn/fns/binary/numeric/mod.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
//!
77
//! [`Binary`]: super::Binary
88
9+
use super::decimal_add_sub_result_dtype;
10+
911
mod decimal;
1012
mod primitive;
1113
#[cfg(test)]
@@ -50,9 +52,15 @@ pub(crate) fn execute_numeric(
5052
}
5153

5254
if lhs.is_empty() {
53-
let result_dtype = lhs
54-
.dtype()
55-
.with_nullability(lhs.dtype().nullability() | rhs.dtype().nullability());
55+
let nullability = lhs.dtype().nullability() | rhs.dtype().nullability();
56+
let result_dtype = match lhs.dtype() {
57+
DType::Primitive(..) => lhs.dtype().with_nullability(nullability),
58+
DType::Decimal(decimal_dtype, _) => {
59+
debug_assert!(matches!(op, NumericOperator::Add | NumericOperator::Sub));
60+
DType::Decimal(decimal_add_sub_result_dtype(*decimal_dtype), nullability)
61+
}
62+
dtype => vortex_bail!("numeric operator is not supported for dtype {}", dtype),
63+
};
5664
return Ok(Canonical::empty(&result_dtype).into_array());
5765
}
5866

0 commit comments

Comments
 (0)