Skip to content

Commit 76a0db4

Browse files
committed
Share decimal value assembly and validate lower-part dtypes
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 13f8cd1 commit 76a0db4

3 files changed

Lines changed: 64 additions & 48 deletions

File tree

‎encodings/decimal-byte-parts/src/decimal_byte_parts/limbs/mod.rs‎

Lines changed: 33 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,13 @@ pub fn assemble_decimal(
252252
let len = msp.len();
253253
let lower: Vec<&[u64]> = lower_parts
254254
.iter()
255-
.map(|part| {
255+
.enumerate()
256+
.map(|(idx, part)| {
257+
vortex_ensure!(
258+
part.dtype() == &LOWER_PART_DTYPE,
259+
"lower part {idx} must have dtype {LOWER_PART_DTYPE}, got {}",
260+
part.dtype()
261+
);
256262
let part = part.as_slice::<u64>();
257263
vortex_ensure!(
258264
part.len() == len,
@@ -265,17 +271,17 @@ pub fn assemble_decimal(
265271

266272
Ok(match lower.as_slice() {
267273
[first] => DecimalArray::new(
268-
assemble_wide::<i128, 1>(msp, [first]),
274+
assemble_wide_decimal::<i128, 1>(msp, [first]),
269275
decimal_dtype,
270276
validity,
271277
),
272278
[first, second] => DecimalArray::new(
273-
assemble_wide::<i256, 2>(msp, [first, second]),
279+
assemble_wide_decimal::<i256, 2>(msp, [first, second]),
274280
decimal_dtype,
275281
validity,
276282
),
277283
[first, second, third] => DecimalArray::new(
278-
assemble_wide::<i256, 3>(msp, [first, second, third]),
284+
assemble_wide_decimal::<i256, 3>(msp, [first, second, third]),
279285
decimal_dtype,
280286
validity,
281287
),
@@ -286,39 +292,10 @@ pub fn assemble_decimal(
286292
})
287293
}
288294

289-
/// Combine a single row's parts into an `i128`.
290-
#[inline]
291-
pub(crate) fn combine_i128(msp: i64, lower: impl IntoIterator<Item = u64>) -> i128 {
292-
lower.into_iter().fold(i128::from(msp), |acc, part| {
293-
(acc << LOWER_PART_BITS) | i128::from(part)
294-
})
295-
}
296-
297-
/// Combine a signed MSP and two or three lower parts into an `i256`.
298-
#[inline]
299-
pub(crate) fn combine_i256(msp: i64, lower: impl ExactSizeIterator<Item = u64>) -> i256 {
300-
let count = lower.len();
301-
let mut high = i128::from(msp);
302-
let mut low = 0u128;
303-
for (index, part) in lower.enumerate() {
304-
if count == 3 && index == 0 {
305-
high = (high << LOWER_PART_BITS) | i128::from(part);
306-
} else {
307-
low = (low << LOWER_PART_BITS) | u128::from(part);
308-
}
309-
}
310-
i256::from_parts(low, high)
311-
}
312-
313-
/// Reassemble a signed MSP and `K` unsigned lower parts into wide integers.
295+
/// Assemble a column of wide decimal values from the MSP and `K` lower-part columns.
314296
///
315-
/// Each row starts with the MSP sign-extended to `T`. Appending a lower word shifts the
316-
/// accumulated value left by 64 bits and fills the low bits with that word. Lower parts
317-
/// are appended most significant first.
318-
///
319-
/// The callers select `i128` for one lower part and `i256` for two or three. Since `K`
320-
/// is constant, the compiler can unroll the loop that appends the lower words.
321-
fn assemble_wide<T, const K: usize>(msp: &PrimitiveArray, lower: [&[u64]; K]) -> Buffer<T>
297+
/// A fixed part count lets the compiler unroll each call to [`assemble_wide_decimal_value`].
298+
fn assemble_wide_decimal<T, const K: usize>(msp: &PrimitiveArray, lower: [&[u64]; K]) -> Buffer<T>
322299
where
323300
T: NativeDecimalType + Shl<usize, Output = T> + BitOr<Output = T>,
324301
{
@@ -329,16 +306,30 @@ where
329306
clippy::useless_conversion,
330307
reason = "the widening to i64 is a no-op only for the i64 arm of the ptype match"
331308
)]
332-
let mut value = T::from(i64::from(*value)).vortex_expect("MSP fits in the output type");
333-
for part in lower {
334-
value = (value << LOWER_PART_BITS)
335-
| T::from(part[row]).vortex_expect("lower word fits in the output type");
336-
}
337-
value
309+
let msp = i64::from(*value);
310+
assemble_wide_decimal_value(msp, lower.map(|part| part[row]))
338311
}));
339312
});
340313
out.freeze()
341314
}
342315

316+
/// Reassemble a decimal's unscaled integer from its signed MSP and `K` lower words.
317+
///
318+
/// Sign-extend the MSP to `T`, then append each lower word by shifting left 64 bits and
319+
/// filling the low bits. Lower words are ordered most significant first. Callers select
320+
/// `i128` for one lower word and `i256` for two or three.
321+
#[inline]
322+
pub(crate) fn assemble_wide_decimal_value<T, const K: usize>(msp: i64, lower: [u64; K]) -> T
323+
where
324+
T: NativeDecimalType + Shl<usize, Output = T> + BitOr<Output = T>,
325+
{
326+
let mut value = T::from(msp).vortex_expect("MSP fits in the output type");
327+
for part in lower {
328+
value = (value << LOWER_PART_BITS)
329+
| T::from(part).vortex_expect("lower word fits in the output type");
330+
}
331+
value
332+
}
333+
343334
#[cfg(test)]
344335
mod tests;

‎encodings/decimal-byte-parts/src/decimal_byte_parts/limbs/tests.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,23 @@ fn test_split_i256_part_order(
181181
Ok(())
182182
}
183183

184+
#[rstest]
185+
#[case::signed(PrimitiveArray::new(buffer![0i64; 2], Validity::NonNullable))]
186+
#[case::narrow_unsigned(PrimitiveArray::new(buffer![0u32; 2], Validity::NonNullable))]
187+
#[case::nullable_all_valid(PrimitiveArray::new(buffer![0u64; 2], Validity::AllValid))]
188+
#[case::nullable_all_null(PrimitiveArray::new(buffer![0u64; 2], Validity::AllInvalid))]
189+
#[case::nullable_mixed(PrimitiveArray::new(buffer![0u64; 2], Validity::from_iter([true, false])))]
190+
fn test_assemble_rejects_invalid_lower_dtype(
191+
#[case] invalid_lower: PrimitiveArray,
192+
#[values(1, 2, 3)] lower_count: usize,
193+
) {
194+
let msp = PrimitiveArray::new(buffer![0i64; 2], Validity::NonNullable);
195+
let mut lower = vec![PrimitiveArray::new(buffer![0u64; 2], Validity::NonNullable); lower_count];
196+
lower[lower_count - 1] = invalid_lower;
197+
let dtype = DecimalDType::new(if lower_count == 1 { 38 } else { 76 }, 0);
198+
assert!(assemble_decimal(&msp, &lower, dtype).is_err());
199+
}
200+
184201
#[rstest]
185202
fn test_assemble_rejects_mismatched_lower_lengths(
186203
#[values(1, 2, 3)] lower_count: usize,

‎encodings/decimal-byte-parts/src/decimal_byte_parts/mod.rs‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ use vortex_session::registry::CachedId;
5757

5858
use crate::decimal_byte_parts::limbs::LOWER_PART_DTYPE;
5959
use crate::decimal_byte_parts::limbs::assemble_decimal;
60-
use crate::decimal_byte_parts::limbs::combine_i128;
61-
use crate::decimal_byte_parts::limbs::combine_i256;
60+
use crate::decimal_byte_parts::limbs::assemble_wide_decimal_value;
6261
use crate::decimal_byte_parts::rules::PARENT_RULES;
6362

6463
/// A [`DecimalByteParts`]-encoded Vortex array.
@@ -440,10 +439,19 @@ impl OperationsVTable<DecimalByteParts> for DecimalByteParts {
440439
})
441440
.collect::<VortexResult<Vec<_>>>()?;
442441

443-
let value = match lower_parts.len() {
444-
0 => DecimalValue::I64(msp),
445-
1 => DecimalValue::I128(combine_i128(msp, lower_parts)),
446-
_ => DecimalValue::I256(combine_i256(msp, lower_parts.into_iter())),
442+
let value = match lower_parts.as_slice() {
443+
[] => DecimalValue::I64(msp),
444+
[first] => DecimalValue::I128(assemble_wide_decimal_value(msp, [*first])),
445+
[first, second] => {
446+
DecimalValue::I256(assemble_wide_decimal_value(msp, [*first, *second]))
447+
}
448+
[first, second, third] => {
449+
DecimalValue::I256(assemble_wide_decimal_value(msp, [*first, *second, *third]))
450+
}
451+
_ => vortex_bail!(
452+
"at most {MAX_LOWER_PARTS} lower parts are supported, got {}",
453+
lower_parts.len()
454+
),
447455
};
448456

449457
Scalar::try_new(array.dtype().clone(), Some(ScalarValue::Decimal(value)))

0 commit comments

Comments
 (0)