Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/codspeed_bench_counts.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"varbinview_try_new": 6,
"varbinview_zip": 2,
"widen": 46,
"widen_decimal": 14
"widen_decimal": 18
},
"vortex-buffer": {
"allocation": 35,
Expand Down
33 changes: 33 additions & 0 deletions vortex-array/benches/widen_decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
//! Comparisons use precision 38 to measure native-width child execution against canonical `i128`
//! execution, including the shortcut for constants outside the child precision. Sum and min-max
//! use the same pair to measure aggregate pushdown without repeating each operation at every width.
//!
//! Casts from precision 38 to 76 measure lazy construction separately from the widening copy on
//! execution. Both canonical and `Widen` inputs must retain the narrow values until execution.

use std::fmt;
use std::sync::LazyLock;
Expand Down Expand Up @@ -327,3 +330,33 @@ fn min_max(bencher: Bencher, case: Case) {
},
);
}

fn cast_target() -> DType {
DType::Decimal(DecimalDType::new(76, SCALE), Nullability::NonNullable)
}

#[divan::bench(args = wide_cases())]
fn cast_lazy(bencher: Bencher, case: Case) {
let values = case.values();
let target = cast_target();
bench_case(
bencher,
|| values.array(case),
|array, _ctx| {
array
.cast(target.clone())
.vortex_expect("a same-scale precision increase is valid")
},
);
}

#[divan::bench(args = wide_cases())]
fn cast_widen(bencher: Bencher, case: Case) {
let values = case.values();
let target = cast_target();
bench_case(
bencher,
|| values.array(case),
|array, ctx| execute_canonical(array.cast(target.clone()), ctx),
);
}
48 changes: 45 additions & 3 deletions vortex-array/src/arrays/decimal/compute/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::arrays::ConstantArray;
use crate::arrays::Decimal;
use crate::arrays::DecimalArray;
use crate::arrays::PrimitiveArray;
use crate::arrays::WidenArray;
use crate::arrays::decimal::DecimalArrayExt;
use crate::dtype::BigCast;
use crate::dtype::DType;
Expand All @@ -44,8 +45,6 @@ use crate::validity::Validity;

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

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

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

if grows_width {
return widen_decimal_values(array, dtype, new_validity).map(Some);
}

// SAFETY: validity has the same length, only its nullability tag changes.
unsafe {
Ok(Some(
Expand Down Expand Up @@ -121,6 +127,10 @@ impl CastKernel for Decimal {
.clone()
.cast_nullability(*to_nullability, array.len(), ctx)?;

if grows_canonical_width(*from_decimal_dtype, *to_decimal_dtype) {
return widen_decimal_values(array, dtype, new_validity).map(Some);
}

// Reuse the values buffer untouched when no rescale is required, the target precision
// only widens (so every value still fits), and the current physical type is already wide
// enough to hold the target precision. This keeps the common precision-widening cast
Expand Down Expand Up @@ -169,6 +179,38 @@ impl CastKernel for Decimal {
}
}

/// Returns whether a cast keeps the scale and grows the canonical width, which implies a higher
/// precision.
fn grows_canonical_width(from: DecimalDType, to: DecimalDType) -> bool {
from.scale() == to.scale() && from.canonical_values_type() < to.canonical_values_type()
}

/// Returns `array` as a decimal `Widen` of `dtype`, over a child with `validity`, which the caller
/// cast to the nullability of `dtype`.
///
/// The child keeps the dtype of `array` as its label, which holds every valid value because reads
/// trust the precision. It also keeps the statistics of `array`, such as exact extrema, because it
/// holds the same values. Only the nullability can change, and only when no value is null.
fn widen_decimal_values(
array: ArrayView<'_, Decimal>,
dtype: &DType,
validity: Validity,
) -> VortexResult<ArrayRef> {
// SAFETY: values and decimal dtype are unchanged, and the caller cast validity without
// changing its length.
let values = unsafe {
DecimalArray::new_unchecked_handle(
array.buffer_handle().clone(),
array.values_type(),
array.decimal_dtype(),
validity,
)
};
values.statistics().inherit_from(array.statistics());

Ok(WidenArray::try_new(values.into_array(), dtype.clone())?.into_array())
}

fn cast_to_integer<T: IntegerPType + BigCast>(
array: ArrayView<'_, Decimal>,
nullability: Nullability,
Expand Down
11 changes: 11 additions & 0 deletions vortex-array/src/arrays/widen/compute/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ use crate::scalar_fn::fns::cast::CastReduce;
impl CastReduce for Widen {
fn cast(array: ArrayView<'_, Self>, dtype: &DType) -> VortexResult<Option<ArrayRef>> {
let values = array.values();
if values.dtype().is_decimal() {
// The child holds exactly the values of the `Widen`, so its cast to the same target
// gives the same result. The decimal cast checks or rescales every value itself, and
// a same-scale cast that grows the canonical width returns a `Widen` again. Other
// targets decline, so execution widens the values before the cast.
return match dtype {
DType::Decimal(..) | DType::Primitive(..) => values.cast(dtype.clone()).map(Some),
_ => Ok(None),
};
}

let (DType::Primitive(target_ptype, nullability), DType::Primitive(values_ptype, _)) =
(dtype, values.dtype())
else {
Expand Down
5 changes: 3 additions & 2 deletions vortex-array/src/arrays/widen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
//! A decimal child has the logical scale and a lower precision, and **must** be valid for its dtype.
//! Comparisons, bounds checks and fills use the child dtype to bound its valid values.
//! [`WidenArray::encode_decimal`] and [`WidenArray::from_stored_decimal`] pick the smallest
//! precision that holds the valid values and the scale. Decimal execution copies the child values
//! into the logical canonical width.
//! precision that holds the valid values and the scale, and a widening decimal cast labels the
//! child with its source dtype. Decimal execution copies the child values into the logical
//! canonical width, without casting, because a widening decimal cast can itself return a `Widen`.
//!
//! Narrowing is a physical choice, so it **must not** change a result. Execution casts the values
//! to the logical dtype for integers, and an operation runs on the values only where it gives the
Expand Down
Loading