Skip to content

Commit 3d1237a

Browse files
committed
Restrict bare decimal byte-parts serde to the frozen format
Reject lower parts when DecimalByteParts is registered directly. Share metadata helpers with DecimalBytePartsPlugin so the plugin continues to support both wire formats without parsing metadata twice. Add regression coverage for bare registration and frozen-format compatibility. Signed-off-by: "Matt Katz" <mhkatz97@gmail.com>
1 parent 9d17f2e commit 3d1237a

2 files changed

Lines changed: 181 additions & 74 deletions

File tree

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

Lines changed: 89 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,53 @@ pub struct DecimalBytesPartsMetadata {
8484
}
8585

8686
impl DecimalBytesPartsMetadata {
87+
fn from_array(array: ArrayView<'_, DecimalByteParts>) -> VortexResult<Self> {
88+
Ok(Self {
89+
zeroth_child_ptype: PType::try_from(array.msp().dtype())? as i32,
90+
lower_part_count: u32::try_from(array.lower_parts().len())
91+
.map_err(|_| vortex_err!("lower part count exceeds u32"))?,
92+
})
93+
}
94+
95+
fn into_array_parts(
96+
self,
97+
dtype: &DType,
98+
len: usize,
99+
children: &dyn ArrayChildren,
100+
) -> VortexResult<ArrayParts<DecimalByteParts>> {
101+
vortex_ensure!(
102+
dtype.as_decimal_opt().is_some(),
103+
"decoding decimal but given non decimal dtype {dtype}"
104+
);
105+
106+
let encoded_dtype = DType::Primitive(self.zeroth_child_ptype(), dtype.nullability());
107+
108+
let lower_part_count = self.lower_part_count()?;
109+
vortex_ensure!(
110+
children.len() == DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
111+
"expected {} children, got {}",
112+
DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
113+
children.len()
114+
);
115+
116+
let msp = children.get(DecimalBytePartsSlots::MSP, &encoded_dtype, len)?;
117+
118+
let mut slots = ArraySlots::with_capacity(children.len());
119+
slots.push(Some(msp));
120+
for idx in 0..lower_part_count {
121+
slots.push(Some(children.get(
122+
DecimalBytePartsSlots::LOWER_PARTS_OFFSET + idx,
123+
&LOWER_PART_DTYPE,
124+
len,
125+
)?));
126+
}
127+
128+
Ok(
129+
ArrayParts::new(DecimalByteParts, dtype.clone(), len, DecimalBytePartsData)
130+
.with_slots(slots),
131+
)
132+
}
133+
87134
/// The number of lower parts encoded in this array.
88135
///
89136
/// # Errors
@@ -155,14 +202,12 @@ impl VTable for DecimalByteParts {
155202
array: ArrayView<'_, Self>,
156203
_session: &VortexSession,
157204
) -> VortexResult<Option<Vec<u8>>> {
158-
let lower_part_count = u32::try_from(array.lower_parts().len())
159-
.map_err(|_| vortex_err!("lower part count exceeds u32"))?;
205+
vortex_ensure!(
206+
array.lower_parts().is_empty(),
207+
"serializing DecimalByteParts with lower parts requires DecimalBytePartsPlugin"
208+
);
160209
Ok(Some(
161-
DecimalBytesPartsMetadata {
162-
zeroth_child_ptype: PType::try_from(array.msp().dtype())? as i32,
163-
lower_part_count,
164-
}
165-
.encode_to_vec(),
210+
DecimalBytesPartsMetadata::from_array(array)?.encode_to_vec(),
166211
))
167212
}
168213

@@ -177,36 +222,10 @@ impl VTable for DecimalByteParts {
177222
) -> VortexResult<ArrayParts<Self>> {
178223
let metadata = DecimalBytesPartsMetadata::decode(metadata)?;
179224
vortex_ensure!(
180-
dtype.as_decimal_opt().is_some(),
181-
"decoding decimal but given non decimal dtype {dtype}"
225+
metadata.lower_part_count()? == 0,
226+
"vortex.decimal_byte_parts must not carry lower parts"
182227
);
183-
184-
let encoded_dtype = DType::Primitive(metadata.zeroth_child_ptype(), dtype.nullability());
185-
186-
let lower_part_count = metadata.lower_part_count()?;
187-
vortex_ensure!(
188-
children.len() == DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
189-
"expected {} children, got {}",
190-
DecimalBytePartsSlots::FIXED_COUNT + lower_part_count,
191-
children.len()
192-
);
193-
194-
let msp = children.get(DecimalBytePartsSlots::MSP, &encoded_dtype, len)?;
195-
196-
let mut slots = ArraySlots::with_capacity(children.len());
197-
slots.push(Some(msp));
198-
for idx in 0..lower_part_count {
199-
slots.push(Some(children.get(
200-
DecimalBytePartsSlots::LOWER_PARTS_OFFSET + idx,
201-
&LOWER_PART_DTYPE,
202-
len,
203-
)?));
204-
}
205-
206-
Ok(
207-
ArrayParts::new(self.clone(), dtype.clone(), len, DecimalBytePartsData)
208-
.with_slots(slots),
209-
)
228+
metadata.into_array_parts(dtype, len, children)
210229
}
211230

212231
fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
@@ -362,6 +381,9 @@ pub fn decimal_byte_parts_v2_id() -> ArrayId {
362381
/// serializes under [`decimal_byte_parts_v2_id`]. Reading holds each ID to its own contract:
363382
/// the frozen ID carries no lower parts and the v2 ID carries at least one, so recognizing the
364383
/// newer format never widens what the frozen one may mean.
384+
///
385+
/// Register this plugin, or call [`crate::initialize`], to enable both formats. Registering
386+
/// [`DecimalByteParts`] directly only supports the frozen format.
365387
#[derive(Clone, Debug)]
366388
pub struct DecimalBytePartsPlugin;
367389

@@ -377,25 +399,33 @@ impl ArrayPlugin for DecimalBytePartsPlugin {
377399
fn serialize(
378400
&self,
379401
array: &ArrayRef,
380-
session: &VortexSession,
402+
_session: &VortexSession,
381403
) -> VortexResult<Option<ArraySerialization>> {
382-
let Some(mut serialization) = ArrayPlugin::serialize(&DecimalByteParts, array, session)?
383-
else {
384-
return Ok(None);
404+
let view = array.as_opt::<DecimalByteParts>().ok_or_else(|| {
405+
vortex_err!(
406+
"DecimalByteParts plugin cannot serialize {}",
407+
array.encoding_id()
408+
)
409+
})?;
410+
let serialized_id = if view.lower_parts().is_empty() {
411+
VTable::id(&DecimalByteParts)
412+
} else {
413+
decimal_byte_parts_v2_id()
385414
};
386-
if !array.as_::<DecimalByteParts>().lower_parts().is_empty() {
387-
serialization.serialized_id = decimal_byte_parts_v2_id();
388-
}
389-
Ok(Some(serialization))
415+
Ok(Some(ArraySerialization::from_array(
416+
serialized_id,
417+
array,
418+
DecimalBytesPartsMetadata::from_array(view)?.encode_to_vec(),
419+
)))
390420
}
391421

392422
fn deserialize(
393423
&self,
394424
parts: ArrayDeserialization<'_>,
395-
session: &VortexSession,
425+
_session: &VortexSession,
396426
) -> VortexResult<ArrayRef> {
397-
let lower_part_count =
398-
DecimalBytesPartsMetadata::decode(parts.metadata)?.lower_part_count()?;
427+
let metadata = DecimalBytesPartsMetadata::decode(parts.metadata)?;
428+
let lower_part_count = metadata.lower_part_count()?;
399429
if parts.serialized_id == decimal_byte_parts_v2_id() {
400430
vortex_ensure!(
401431
lower_part_count > 0,
@@ -414,14 +444,10 @@ impl ArrayPlugin for DecimalBytePartsPlugin {
414444
parts.serialized_id
415445
);
416446
}
417-
Ok(Array::try_from_parts(VTable::deserialize(
418-
&DecimalByteParts,
447+
Ok(Array::try_from_parts(metadata.into_array_parts(
419448
parts.dtype,
420449
parts.len,
421-
parts.metadata,
422-
parts.buffers,
423450
parts.children,
424-
session,
425451
)?)?
426452
.into_array())
427453
}
@@ -897,26 +923,19 @@ mod tests {
897923
fn deserialize_with(
898924
lower_part_count: u32,
899925
children: Vec<ArrayRef>,
900-
) -> VortexResult<ArrayParts<DecimalByteParts>> {
901-
let metadata = DecimalBytesPartsMetadata {
902-
zeroth_child_ptype: PType::I64 as i32,
903-
lower_part_count,
926+
) -> VortexResult<DecimalBytePartsArray> {
927+
let serialized_id = if lower_part_count == 0 {
928+
VTable::id(&DecimalByteParts)
929+
} else {
930+
decimal_byte_parts_v2_id()
904931
};
905-
VTable::deserialize(
906-
&DecimalByteParts,
907-
&DType::Decimal(DecimalDType::new(38, 2), Nullability::NonNullable),
908-
3,
909-
&metadata.encode_to_vec(),
910-
&[],
911-
&children,
912-
&array_session(),
913-
)
932+
plugin_deserialize_with(serialized_id, lower_part_count, children)
933+
.map(|array| array.as_::<DecimalByteParts>().into_owned())
914934
}
915935

916936
#[test]
917937
fn test_deserialize_reads_lower_parts() -> VortexResult<()> {
918-
let parts = deserialize_with(1, vec![msp(), lower_part()])?;
919-
let array = Array::try_from_parts(parts)?;
938+
let array = deserialize_with(1, vec![msp(), lower_part()])?;
920939
assert_eq!(array.lower_parts().len(), 1);
921940

922941
let mut ctx = array_session().create_execution_ctx();
@@ -936,9 +955,7 @@ mod tests {
936955
let session = array_session();
937956
crate::initialize(&session);
938957

939-
let array = deserialize_with(1, vec![msp(), lower_part()])
940-
.and_then(Array::try_from_parts)?
941-
.into_array();
958+
let array = deserialize_with(1, vec![msp(), lower_part()])?.into_array();
942959

943960
let serialization = session
944961
.array_serialize(&array)?
@@ -968,9 +985,7 @@ mod tests {
968985
let mut ctx = session.create_execution_ctx();
969986

970987
// Stands in for an array materialized from a file: the parts already exist.
971-
let array = deserialize_with(1, vec![msp(), lower_part()])
972-
.and_then(Array::try_from_parts)?
973-
.into_array();
988+
let array = deserialize_with(1, vec![msp(), lower_part()])?.into_array();
974989

975990
let sliced = array.slice(0..2)?;
976991
assert_eq!(sliced.execute::<DecimalArray>(&mut ctx)?.len(), 2);
@@ -984,7 +999,7 @@ mod tests {
984999
let mut children = vec![buffer![0i64; 3].into_array()];
9851000
children.extend((1..lower_part_count).map(|_| buffer![0u64; 3].into_array()));
9861001
children.push(lower_part());
987-
let array = Array::try_from_parts(deserialize_with(lower_part_count, children)?)?;
1002+
let array = deserialize_with(lower_part_count, children)?;
9881003
let expected = DecimalArray::new(
9891004
buffer![1i128, 2, 3],
9901005
DecimalDType::new(38, 2),

‎encodings/decimal-byte-parts/tests/format_v2.rs‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
#![expect(clippy::tests_outside_test_module)]
1515

1616
use vortex_array::ArrayContext;
17+
use vortex_array::ArrayDeserialization;
1718
use vortex_array::ArrayId;
1819
use vortex_array::ArrayRef;
1920
use vortex_array::ArrayVTable;
2021
use vortex_array::IntoArray;
22+
use vortex_array::VortexSessionExecute;
23+
use vortex_array::assert_arrays_eq;
2124
use vortex_array::dtype::DecimalDType;
2225
use vortex_array::serde::SerializeOptions;
2326
use vortex_array::session::ArraySessionExt;
@@ -163,3 +166,92 @@ fn wide_format_is_refused_where_not_permitted() -> VortexResult<()> {
163166

164167
Ok(())
165168
}
169+
170+
#[test]
171+
fn bare_vtable_refuses_wide_serialization() -> VortexResult<()> {
172+
let session = vortex_array::array_session();
173+
session.arrays().register(DecimalByteParts);
174+
let array = DecimalByteParts::try_new_with_lower_parts(
175+
msp(),
176+
vec![lower_part()],
177+
DecimalDType::new(38, 2),
178+
)?
179+
.into_array();
180+
let restricted = ArrayContext::empty().with_allowed_ids(
181+
[
182+
ArrayVTable::id(&DecimalByteParts),
183+
ArrayVTable::id(&vortex_array::arrays::Primitive),
184+
]
185+
.into_iter()
186+
.collect(),
187+
);
188+
189+
assert!(
190+
array
191+
.serialize(&restricted, &session, &SerializeOptions::default())
192+
.is_err(),
193+
"bare VTable registration must not write lower parts under the frozen ID"
194+
);
195+
Ok(())
196+
}
197+
198+
#[test]
199+
fn bare_vtable_refuses_lower_parts_on_frozen_id() -> VortexResult<()> {
200+
let session = vortex_array::array_session();
201+
session.arrays().register(DecimalByteParts);
202+
let array = DecimalByteParts::try_new_with_lower_parts(
203+
msp(),
204+
vec![lower_part()],
205+
DecimalDType::new(38, 2),
206+
)?
207+
.into_array();
208+
let id = ArrayVTable::id(&DecimalByteParts);
209+
let plugin = session
210+
.arrays()
211+
.registry()
212+
.get(&id)
213+
.ok_or_else(|| vortex_err!("missing decimal plugin"))?;
214+
let children = array.children();
215+
216+
// i64 MSP and one lower part, mislabeled as the frozen format.
217+
let parts = ArrayDeserialization::new(
218+
id,
219+
array.dtype(),
220+
array.len(),
221+
&[8, 7, 16, 1],
222+
&[],
223+
&children,
224+
);
225+
assert!(plugin.deserialize(parts, &session).is_err());
226+
Ok(())
227+
}
228+
229+
#[test]
230+
fn bare_vtable_keeps_frozen_serde() -> VortexResult<()> {
231+
let session = vortex_array::array_session();
232+
session.arrays().register(DecimalByteParts);
233+
let array = DecimalByteParts::try_new(msp(), DecimalDType::new(2, 0))?.into_array();
234+
let serialized = session
235+
.array_serialize(&array)?
236+
.ok_or_else(|| vortex_err!("missing decimal serialization"))?;
237+
assert_eq!(serialized.serialized_id, ArrayVTable::id(&DecimalByteParts));
238+
assert_eq!(serialized.metadata, [8, 7]);
239+
let plugin = session
240+
.arrays()
241+
.registry()
242+
.get(&serialized.serialized_id)
243+
.ok_or_else(|| vortex_err!("missing decimal plugin"))?;
244+
let decoded = plugin.deserialize(
245+
ArrayDeserialization::new(
246+
serialized.serialized_id,
247+
array.dtype(),
248+
array.len(),
249+
&serialized.metadata,
250+
&[],
251+
&serialized.children,
252+
),
253+
&session,
254+
)?;
255+
assert_arrays_eq!(array, decoded, &mut session.create_execution_ctx());
256+
Ok(())
257+
}

0 commit comments

Comments
 (0)