Skip to content

Commit 7819bca

Browse files
committed
Keep same-scale decimal widening casts lazy
A same-scale cast that grows the canonical width copied every value into the wider type, even when the caller never reads them all. Return a decimal Widen over the source instead, whose dtype is a true label for its values, so the copy waits for execution. The child also keeps the statistics of the source, such as exact extrema, because it holds the same values. A cast of a decimal Widen passes to its child. This is the first production code that builds a decimal Widen. Widen execution already copies decimal values instead of yielding a cast, which now also keeps it from yielding a cast that returns a Widen again. A test checks that the default file writer round-trips a decimal Widen. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent e50341e commit 7819bca

9 files changed

Lines changed: 429 additions & 10 deletions

File tree

‎vortex-array/benches/widen_decimal.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@ impl Case {
205205
10i64.pow(u32::from(self.child_precision))
206206
}
207207

208+
/// Returns a same-scale target with a wider canonical width than the parent, so the cast of a
209+
/// `Widen` stays lazy (see `grows_canonical_width`). A target at the same canonical width would
210+
/// measure a different cast path.
208211
fn cast_target(self) -> DType {
209212
let precision = if self.precision == 38 { 76 } else { 38 };
210213
DType::Decimal(

‎vortex-array/src/arrays/decimal/compute/cast.rs‎

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::arrays::ConstantArray;
2121
use crate::arrays::Decimal;
2222
use crate::arrays::DecimalArray;
2323
use crate::arrays::PrimitiveArray;
24+
use crate::arrays::WidenArray;
2425
use crate::arrays::decimal::DecimalArrayExt;
2526
use crate::dtype::BigCast;
2627
use crate::dtype::DType;
@@ -44,8 +45,6 @@ use crate::validity::Validity;
4445

4546
impl CastReduce for Decimal {
4647
fn cast(array: ArrayView<'_, Decimal>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
47-
// Only nullability changes within the same decimal dtype are reducible without execution.
48-
// Precision/scale changes need the kernel.
4948
let DType::Decimal(to_decimal_dtype, to_nullability) = dtype else {
5049
return Ok(None);
5150
};
@@ -56,7 +55,10 @@ impl CastReduce for Decimal {
5655
);
5756
};
5857

59-
if from_decimal_dtype != to_decimal_dtype {
58+
// Without execution, only a nullability change and a same-scale cast that grows the
59+
// canonical width reduce. Every other change of the decimal dtype needs the kernel.
60+
let grows_width = grows_canonical_width(*from_decimal_dtype, *to_decimal_dtype);
61+
if from_decimal_dtype != to_decimal_dtype && !grows_width {
6062
return Ok(None);
6163
}
6264

@@ -67,6 +69,10 @@ impl CastReduce for Decimal {
6769
return Ok(None);
6870
};
6971

72+
if grows_width {
73+
return widen_decimal_values(array, dtype, new_validity).map(Some);
74+
}
75+
7076
// SAFETY: validity has the same length, only its nullability tag changes.
7177
unsafe {
7278
Ok(Some(
@@ -121,6 +127,10 @@ impl CastKernel for Decimal {
121127
.clone()
122128
.cast_nullability(*to_nullability, array.len(), ctx)?;
123129

130+
if grows_canonical_width(*from_decimal_dtype, *to_decimal_dtype) {
131+
return widen_decimal_values(array, dtype, new_validity).map(Some);
132+
}
133+
124134
// Reuse the values buffer untouched when no rescale is required, the target precision
125135
// only widens (so every value still fits), and the current physical type is already wide
126136
// enough to hold the target precision. This keeps the common precision-widening cast
@@ -169,6 +179,38 @@ impl CastKernel for Decimal {
169179
}
170180
}
171181

182+
/// Returns whether a cast keeps the scale and grows the canonical width, which implies a higher
183+
/// precision.
184+
fn grows_canonical_width(from: DecimalDType, to: DecimalDType) -> bool {
185+
from.scale() == to.scale() && from.canonical_values_type() < to.canonical_values_type()
186+
}
187+
188+
/// Returns `array` as a decimal `Widen` of `dtype`, over a child with `validity`, which the caller
189+
/// cast to the nullability of `dtype`.
190+
///
191+
/// The child keeps the dtype of `array` as its label, which holds every valid value because reads
192+
/// trust the precision. It also keeps the statistics of `array`, such as exact extrema, because it
193+
/// holds the same values. Only the nullability can change, and only when no value is null.
194+
fn widen_decimal_values(
195+
array: ArrayView<'_, Decimal>,
196+
dtype: &DType,
197+
validity: Validity,
198+
) -> VortexResult<ArrayRef> {
199+
// SAFETY: values and decimal dtype are unchanged, and the caller cast validity without
200+
// changing its length.
201+
let values = unsafe {
202+
DecimalArray::new_unchecked_handle(
203+
array.buffer_handle().clone(),
204+
array.values_type(),
205+
array.decimal_dtype(),
206+
validity,
207+
)
208+
};
209+
values.statistics().inherit_from(array.statistics());
210+
211+
Ok(WidenArray::try_new(values.into_array(), dtype.clone())?.into_array())
212+
}
213+
172214
fn cast_to_integer<T: IntegerPType + BigCast>(
173215
array: ArrayView<'_, Decimal>,
174216
nullability: Nullability,

‎vortex-array/src/arrays/widen/compute/cast.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ use crate::scalar_fn::fns::cast::CastReduce;
1616
impl CastReduce for Widen {
1717
fn cast(array: ArrayView<'_, Self>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
1818
let values = array.values();
19+
if matches!(values.dtype(), DType::Decimal(..)) {
20+
// The child holds exactly the values of the `Widen`, so its cast to the same target
21+
// gives the same result. The decimal cast checks or rescales every value itself, and
22+
// a same-scale cast that grows the canonical width returns a `Widen` again. Other
23+
// targets decline, so execution widens the values before the cast.
24+
return match dtype {
25+
DType::Decimal(..) | DType::Primitive(..) => values.cast(dtype.clone()).map(Some),
26+
_ => Ok(None),
27+
};
28+
}
29+
1930
let (DType::Primitive(target_ptype, nullability), DType::Primitive(values_ptype, _)) =
2031
(dtype, values.dtype())
2132
else {

‎vortex-array/src/arrays/widen/mod.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
//! values cannot check: the child precision **must** hold every valid value. Compare, between and
1515
//! fill null decide from the child dtype alone, so a false label gives wrong results.
1616
//! [`WidenArray::encode_decimal`] and [`WidenArray::from_stored_decimal`] pick the smallest
17-
//! precision that holds the valid values and the scale. Decimal execution copies the child values
18-
//! into the logical canonical width.
17+
//! precision that holds the valid values and the scale, and a widening decimal cast labels the
18+
//! child with its source dtype. Decimal execution copies the child values into the logical
19+
//! canonical width, without casting, because a widening decimal cast can itself return a `Widen`.
1920
//!
2021
//! Narrowing is a physical choice, so it **must not** change a result. Execution casts the values
2122
//! to the logical dtype for integers, and an operation runs on the values only where it gives the

0 commit comments

Comments
 (0)