Skip to content

Commit bbd1a50

Browse files
authored
Add ArrayPlugin to enable serde for new DecimalBytePartsArray format (#9810)
## Summary Add serialization for `DecimalBytePartsArray` with lower parts while preserving the frozen v1 file format. `DecimalBytePartsPlugin` owns both formats and selects the serialized ID from the array's layout: | Array layout | Serialized ID | | --- | --- | | Signed most significant part (MSP) only | `vortex.decimal_byte_parts` | | Signed MSP plus one to three unsigned lower parts | `vortex.decimal_byte_parts_v2` | Both formats deserialize into the same in-memory array representation. Each lower part represents a 64-bit window, but its storage dtype may be narrowed to `u8`, `u16`, or `u32` when its values fit. ## Changes - Record the MSP's physical integer type, lower-part count, and each lower part's physical integer type in metadata. Deserialization validates these types and restores each child with its recorded dtype. - Keep the frozen format restricted to zero lower parts and require at least one for v2. Frozen metadata remains byte-identical. - Make DBP's VTable `serialize` and `deserialize` methods return errors directing callers to `DecimalBytePartsPlugin`. Keep DBP metadata and serde helpers in `plugin.rs` so the dedicated plugin owns both formats. - Add a separate v2 compatibility fixture covering positive and negative wide `i128` values and nullable `i256` values. The frozen fixture is unchanged. ## API Changes **Breaking:** directly registering `DecimalByteParts`, or calling its VTable serde methods, now errors for both serialization and deserialization, including the frozen v1 format. Replace: ```rust session.arrays().register(DecimalByteParts); ``` with: ```rust session.arrays().register(DecimalBytePartsPlugin); ``` `vortex_decimal_byte_parts::initialize(&session)` already registers the plugin. Existing v1 files remain readable through it; no file migration is required. --------- Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 8fc58e0 commit bbd1a50

20 files changed

Lines changed: 810 additions & 265 deletions

File tree

‎docs/specs/editions.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,9 @@ representation gains support for wide decimals, represented by a signed most-sig
172172
represented that way, it emits `vortex.decimal_byte_parts` with `lower_part_count = 0`, even if
173173
the current in-memory array has lower-part children.
174174
- An array that cannot be collapsed into that old form losslessly uses the new
175-
`vortex.decimal_byte_parts_v2` component, initially staged in a draft edition.
175+
`vortex.decimal_byte_parts.v2` component, initially staged in a draft edition.
176176
- A new reader deserializes both IDs into the same in-memory representation. An older reader reports
177-
`vortex.decimal_byte_parts_v2` as unknown instead of trying to decode a wire format it does not support.
177+
`vortex.decimal_byte_parts.v2` as unknown instead of trying to decode a wire format it does not support.
178178
- When targeting an edition that permits only the old ID, serializing a value that can be collapsed succeeds; an
179179
irreducibly multi-part value fails because no lossless downgrade exists.
180180

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

Lines changed: 16 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ use std::fmt::Display;
77
use std::fmt::Formatter;
88
use std::hash::Hasher;
99

10-
use prost::Message as _;
1110
use vortex_array::Array;
1211
use vortex_array::ArrayEq;
1312
use vortex_array::ArrayHash;
1413
use vortex_array::ArrayId;
1514
use vortex_array::ArrayParts;
1615
use vortex_array::ArrayRef;
17-
use vortex_array::ArraySlots;
1816
use vortex_array::ArrayView;
1917
use vortex_array::EqMode;
2018
use vortex_array::ExecutionCtx;
@@ -24,7 +22,6 @@ use vortex_array::array_slots;
2422
use vortex_array::buffer::BufferHandle;
2523
use vortex_array::dtype::DType;
2624
use vortex_array::dtype::DecimalDType;
27-
use vortex_array::dtype::PType;
2825
use vortex_array::scalar::DecimalValue;
2926
use vortex_array::scalar::Scalar;
3027
use vortex_array::scalar::ScalarValue;
@@ -37,92 +34,18 @@ use vortex_error::VortexExpect;
3734
use vortex_error::VortexResult;
3835
use vortex_error::vortex_bail;
3936
use vortex_error::vortex_ensure;
40-
use vortex_error::vortex_err;
4137
use vortex_error::vortex_panic;
4238
use vortex_session::VortexSession;
43-
use vortex_session::registry::CachedId;
4439

45-
use super::LOWER_PART_DTYPE;
4640
use super::MAX_LOWER_PARTS;
4741
use super::assemble::assemble_decimal;
4842
use super::assemble::assemble_wide_decimal_value;
43+
use super::decimal_byte_parts_v2_id;
4944
use super::rules::PARENT_RULES;
5045

5146
/// A [`DecimalByteParts`]-encoded Vortex array.
5247
pub type DecimalBytePartsArray = Array<DecimalByteParts>;
5348

54-
#[derive(Clone, prost::Message)]
55-
pub struct DecimalBytesPartsMetadata {
56-
#[prost(enumeration = "PType", tag = "1")]
57-
zeroth_child_ptype: i32,
58-
#[prost(uint32, tag = "2")]
59-
lower_part_count: u32,
60-
}
61-
62-
impl DecimalBytesPartsMetadata {
63-
fn from_array(array: ArrayView<'_, DecimalByteParts>) -> VortexResult<Self> {
64-
Ok(Self {
65-
zeroth_child_ptype: PType::try_from(array.msp().dtype())? as i32,
66-
lower_part_count: u32::try_from(array.lower_parts().len())
67-
.map_err(|_| vortex_err!("lower part count exceeds u32"))?,
68-
})
69-
}
70-
71-
fn into_array_parts(
72-
self,
73-
dtype: &DType,
74-
len: usize,
75-
children: &dyn ArrayChildren,
76-
) -> VortexResult<ArrayParts<DecimalByteParts>> {
77-
vortex_ensure!(
78-
dtype.as_decimal_opt().is_some(),
79-
"decoding decimal but given non decimal dtype {dtype}"
80-
);
81-
82-
let encoded_dtype = DType::Primitive(self.zeroth_child_ptype(), dtype.nullability());
83-
84-
let lower_part_count = self.lower_part_count()?;
85-
vortex_ensure!(
86-
children.len() == DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
87-
"expected {} children, got {}",
88-
DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
89-
children.len()
90-
);
91-
92-
let msp = children.get(DecimalBytePartsSlots::MSP, &encoded_dtype, len)?;
93-
94-
let mut slots = ArraySlots::with_capacity(children.len());
95-
slots.push(Some(msp));
96-
for idx in 0..lower_part_count {
97-
slots.push(Some(children.get(
98-
DecimalBytePartsSlots::LOWER_PARTS_OFFSET + idx,
99-
&LOWER_PART_DTYPE,
100-
len,
101-
)?));
102-
}
103-
104-
Ok(
105-
ArrayParts::new(DecimalByteParts, dtype.clone(), len, DecimalBytePartsData)
106-
.with_slots(slots),
107-
)
108-
}
109-
110-
/// The number of lower parts encoded in this array.
111-
///
112-
/// # Errors
113-
///
114-
/// Returns an error if the count exceeds [`MAX_LOWER_PARTS`].
115-
fn lower_part_count(&self) -> VortexResult<usize> {
116-
let count = usize::try_from(self.lower_part_count)
117-
.map_err(|_| vortex_err!("lower part count {} out of range", self.lower_part_count))?;
118-
vortex_ensure!(
119-
count <= MAX_LOWER_PARTS,
120-
"at most {MAX_LOWER_PARTS} lower parts are supported, got {count}"
121-
);
122-
Ok(count)
123-
}
124-
}
125-
12649
/// This array encodes decimals by splitting them between 1-4 columns of primitive typed children.
12750
///
12851
/// The most significant part (MSP) stores the most significant decimal bits. It is signed and is
@@ -222,7 +145,7 @@ impl DecimalByteParts {
222145
///
223146
/// Lower parts are ordered most significant first and must each be a non-nullable unsigned integer
224147
/// array of the same length as the MSP. See [`super::split_decimal`] for producing them from a
225-
/// canonical decimal array.
148+
/// decimal array.
226149
///
227150
/// # Errors
228151
///
@@ -274,8 +197,7 @@ impl VTable for DecimalByteParts {
274197
type ValidityVTable = ValidityVTableFromChild;
275198

276199
fn id(&self) -> ArrayId {
277-
static ID: CachedId = CachedId::new("vortex.decimal_byte_parts");
278-
*ID
200+
decimal_byte_parts_v2_id()
279201
}
280202

281203
fn validate(
@@ -331,33 +253,22 @@ impl VTable for DecimalByteParts {
331253
}
332254

333255
fn serialize(
334-
array: ArrayView<'_, Self>,
256+
_array: ArrayView<'_, Self>,
335257
_session: &VortexSession,
336258
) -> VortexResult<Option<Vec<u8>>> {
337-
vortex_ensure!(
338-
array.lower_parts().is_empty(),
339-
"serializing DecimalByteParts with lower parts is not supported"
340-
);
341-
Ok(Some(
342-
DecimalBytesPartsMetadata::from_array(array)?.encode_to_vec(),
343-
))
259+
vortex_bail!("DecimalByteParts serialization requires DecimalBytePartsPlugin")
344260
}
345261

346262
fn deserialize(
347263
&self,
348-
dtype: &DType,
349-
len: usize,
350-
metadata: &[u8],
264+
_dtype: &DType,
265+
_len: usize,
266+
_metadata: &[u8],
351267
_buffers: &[BufferHandle],
352-
children: &dyn ArrayChildren,
268+
_children: &dyn ArrayChildren,
353269
_session: &VortexSession,
354270
) -> VortexResult<ArrayParts<Self>> {
355-
let metadata = DecimalBytesPartsMetadata::decode(metadata)?;
356-
vortex_ensure!(
357-
metadata.lower_part_count()? == 0,
358-
"vortex.decimal_byte_parts must not carry lower parts"
359-
);
360-
metadata.into_array_parts(dtype, len, children)
271+
vortex_bail!("DecimalByteParts deserialization requires DecimalBytePartsPlugin")
361272
}
362273

363274
fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
@@ -508,18 +419,15 @@ mod tests {
508419
use vortex_array::scalar::Scalar;
509420
use vortex_array::scalar::ScalarValue;
510421
use vortex_array::validity::Validity;
511-
use vortex_array::vtable::VTable;
512422
use vortex_buffer::buffer;
513423
use vortex_error::VortexResult;
514424

515425
use super::DecimalByteParts;
516-
use super::DecimalBytePartsArray;
517426
use super::DecimalBytePartsArraySlotsExt;
518427
use super::DecimalBytePartsData;
519428
use crate::decimal_byte_parts::LOWER_PART_DTYPE;
520429
use crate::decimal_byte_parts::MAX_LOWER_PARTS;
521430
use crate::decimal_byte_parts::testing::i128_parts;
522-
use crate::decimal_byte_parts::testing::i256_of;
523431
use crate::decimal_byte_parts::testing::i256_parts;
524432

525433
#[test]
@@ -561,62 +469,6 @@ mod tests {
561469
);
562470
}
563471

564-
/// The largest unscaled value a `Decimal(38, _)` can hold: `10^38 - 1`.
565-
const MAX_PRECISION_38: i128 = 99_999_999_999_999_999_999_999_999_999_999_999_999;
566-
567-
/// The largest unscaled value a `Decimal(76, _)` can hold: `10^76 - 1`.
568-
fn max_precision_76() -> i256 {
569-
i256::from_i128(10).wrapping_pow(76) - i256::ONE
570-
}
571-
572-
/// Values that exercise every 64-bit window of an `i128`, both signs, and the boundaries
573-
/// where a lower part carries into the MSP.
574-
fn wide_i128_values() -> Vec<i128> {
575-
vec![
576-
0,
577-
1,
578-
-1,
579-
(1 << 64) - 1,
580-
1 << 64,
581-
-(1 << 64),
582-
-((1 << 64) + 1),
583-
MAX_PRECISION_38,
584-
-MAX_PRECISION_38,
585-
1 << 100,
586-
]
587-
}
588-
589-
/// Values that exercise every 64-bit window of an `i256`.
590-
fn wide_i256_values() -> Vec<i256> {
591-
vec![
592-
i256::ZERO,
593-
i256::ONE,
594-
i256::ZERO - i256::ONE,
595-
i256_of(0, u128::MAX),
596-
i256_of(1, 0),
597-
i256_of(-1, 0),
598-
i256_of(-1, u128::MAX - 1),
599-
i256_of(1 << 64, 12345),
600-
max_precision_76(),
601-
i256::ZERO - max_precision_76(),
602-
]
603-
}
604-
605-
#[rstest]
606-
#[case::i128_non_nullable(i128_parts(wide_i128_values(), Validity::NonNullable))]
607-
#[case::i256_non_nullable(i256_parts(wide_i256_values(), Validity::NonNullable))]
608-
fn test_canonical_decimal_round_trips(
609-
#[case] array: DecimalBytePartsArray,
610-
) -> VortexResult<()> {
611-
let mut ctx = array_session().create_execution_ctx();
612-
let canonical = array
613-
.clone()
614-
.into_array()
615-
.execute::<DecimalArray>(&mut ctx)?;
616-
assert_arrays_eq!(array, canonical, &mut ctx);
617-
Ok(())
618-
}
619-
620472
#[test]
621473
fn test_lower_part_layout_i128() -> VortexResult<()> {
622474
let array = i128_parts(vec![(3i128 << 64) | 7], Validity::NonNullable);
@@ -637,7 +489,7 @@ mod tests {
637489
#[test]
638490
fn test_lower_part_layout_i256() -> VortexResult<()> {
639491
let array = i256_parts(
640-
vec![i256_of((5i128 << 64) | 6, (7u128 << 64) | 8)],
492+
vec![i256::from_parts((7u128 << 64) | 8, (5i128 << 64) | 6)],
641493
Validity::NonNullable,
642494
);
643495
assert_eq!(array.lower_parts().len(), MAX_LOWER_PARTS);
@@ -652,27 +504,6 @@ mod tests {
652504
Ok(())
653505
}
654506

655-
#[rstest]
656-
#[case::i128(i128_parts(wide_i128_values(), Validity::AllValid))]
657-
#[case::i256(i256_parts(wide_i256_values(), Validity::AllValid))]
658-
fn test_scalar_at_matches_canonical(#[case] array: DecimalBytePartsArray) -> VortexResult<()> {
659-
let mut ctx = array_session().create_execution_ctx();
660-
let canonical = array
661-
.clone()
662-
.into_array()
663-
.execute::<DecimalArray>(&mut ctx)?
664-
.into_array();
665-
let array = array.into_array();
666-
for idx in 0..array.len() {
667-
assert_eq!(
668-
array.execute_scalar(idx, &mut ctx)?,
669-
canonical.execute_scalar(idx, &mut ctx)?,
670-
"scalar mismatch at index {idx}"
671-
);
672-
}
673-
Ok(())
674-
}
675-
676507
#[rstest]
677508
fn test_scalar_at_matches_canonical_for_each_part_count(
678509
#[values(false, true)] narrow_msp: bool,
@@ -858,7 +689,7 @@ mod tests {
858689
let canonical = i128_array.into_array().execute::<DecimalArray>(&mut ctx)?;
859690
assert_eq!(canonical.values_type(), DecimalType::I128);
860691

861-
let i256_array = i256_parts(vec![i256_of(1 << 100, 0)], Validity::NonNullable);
692+
let i256_array = i256_parts(vec![i256::from_parts(0, 1 << 100)], Validity::NonNullable);
862693
let canonical = i256_array.into_array().execute::<DecimalArray>(&mut ctx)?;
863694
assert_eq!(canonical.values_type(), DecimalType::I256);
864695

@@ -883,7 +714,10 @@ mod tests {
883714
)?;
884715
let canonical = array.into_array().execute::<DecimalArray>(&mut ctx)?;
885716
assert_eq!(canonical.values_type(), DecimalType::I256);
886-
assert_eq!(canonical.buffer::<i256>().as_slice(), &[i256_of(1, 9)]);
717+
assert_eq!(
718+
canonical.buffer::<i256>().as_slice(),
719+
&[i256::from_parts(9, 1)]
720+
);
887721
Ok(())
888722
}
889723

@@ -910,11 +744,4 @@ mod tests {
910744
assert_arrays_eq!(array, canonical.into_array(), &mut ctx);
911745
Ok(())
912746
}
913-
#[test]
914-
fn test_frozen_serializer_rejects_lower_parts() -> VortexResult<()> {
915-
let session = array_session();
916-
let array = i128_parts(vec![1i128 << 70], Validity::NonNullable);
917-
assert!(VTable::serialize(array.as_view(), &session).is_err());
918-
Ok(())
919-
}
920747
}

‎encodings/decimal-byte-parts/src/decimal_byte_parts/compute/filter.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ mod test {
2727
use vortex_array::arrays::PrimitiveArray;
2828
use vortex_array::compute::conformance::filter::test_filter_conformance;
2929
use vortex_array::dtype::DecimalDType;
30+
use vortex_array::dtype::i256;
3031
use vortex_array::validity::Validity;
3132
use vortex_buffer::buffer;
3233

3334
use crate::DecimalByteParts;
3435
use crate::decimal_byte_parts::testing::i128_parts;
35-
use crate::decimal_byte_parts::testing::i256_of;
3636
use crate::decimal_byte_parts::testing::i256_parts;
3737

3838
#[test]
@@ -72,11 +72,11 @@ mod test {
7272

7373
let array = i256_parts(
7474
vec![
75-
i256_of(1, 0),
76-
i256_of(-1, 5),
77-
i256_of(0, u128::MAX),
78-
i256_of(1 << 64, 7),
79-
i256_of(0, 0),
75+
i256::from_parts(0, 1),
76+
i256::from_parts(5, -1),
77+
i256::from_parts(u128::MAX, 0),
78+
i256::from_parts(7, 1 << 64),
79+
i256::from_parts(0, 0),
8080
],
8181
Validity::from_iter([true, false, true, true, false]),
8282
);

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ mod tests {
2727
use crate::DecimalByteParts;
2828
use crate::DecimalBytePartsArray;
2929
use crate::decimal_byte_parts::testing::i128_parts;
30-
use crate::decimal_byte_parts::testing::i256_of;
3130
use crate::decimal_byte_parts::testing::i256_parts;
3231

3332
/// Values needing more than 64 bits, so the encoding carries lower parts.
@@ -43,11 +42,11 @@ mod tests {
4342

4443
fn wide_i256() -> Vec<i256> {
4544
vec![
46-
i256_of(1, 0),
47-
i256_of(-1, 0),
48-
i256_of(0, u128::MAX),
49-
i256_of(1 << 64, 7),
50-
i256_of(0, 0),
45+
i256::from_parts(0, 1),
46+
i256::from_parts(0, -1),
47+
i256::from_parts(u128::MAX, 0),
48+
i256::from_parts(7, 1 << 64),
49+
i256::from_parts(0, 0),
5150
]
5251
}
5352

0 commit comments

Comments
 (0)