Skip to content

Commit f3d53cc

Browse files
committed
Write Widen arrays through an array plugin
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 851cd14 commit f3d53cc

8 files changed

Lines changed: 369 additions & 4 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
//! Take executes its gather instead of leaving a lazy [`Dict`] under a `Widen`, because code such
1717
//! as the DuckDB exporter and the dict layout writer looks for a `Dict` at the root of an array.
1818
//!
19-
//! `Widen` has no wire form yet, so the default [`ArraySession`] does not register it, and an
20-
//! array that holds a `Widen` cannot be serialized.
19+
//! `Widen` has no wire form yet. The default [`ArraySession`] registers [`WidenPlugin`], which
20+
//! writes each `Widen` as its canonical integer array. Serialization keeps the `Widen` statistics,
21+
//! and readers never see `vortex.widen`.
2122
//!
2223
//! [`ArraySession`]: crate::session::ArraySession
2324
//! [`Dict`]: crate::arrays::Dict
@@ -35,6 +36,7 @@ mod encode;
3536
mod vtable;
3637
use vortex_session::VortexSession;
3738
pub use vtable::Widen;
39+
pub use vtable::WidenPlugin;
3840

3941
use crate::array::VTable;
4042
use crate::arrays::Dict;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod aggregates;
1010
mod compute;
1111
mod encoding;
1212
mod numeric;
13+
mod serde;
1314

1415
use std::sync::LazyLock;
1516

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use rstest::rstest;
5+
use vortex_buffer::ByteBuffer;
6+
use vortex_buffer::ByteBufferMut;
7+
use vortex_buffer::buffer;
8+
use vortex_error::VortexExpect;
9+
use vortex_error::VortexResult;
10+
use vortex_session::VortexSession;
11+
use vortex_session::registry::ReadContext;
12+
13+
use super::SESSION;
14+
use crate::ArrayContext;
15+
use crate::ArrayId;
16+
use crate::ArrayRef;
17+
use crate::ArrayVTable;
18+
use crate::IntoArray;
19+
use crate::VortexSessionExecute;
20+
use crate::arrays::Bool;
21+
use crate::arrays::ChunkedArray;
22+
use crate::arrays::Primitive;
23+
use crate::arrays::PrimitiveArray;
24+
use crate::arrays::StructArray;
25+
use crate::arrays::Widen;
26+
use crate::arrays::WidenArray;
27+
use crate::arrays::widen::WidenPlugin;
28+
use crate::assert_arrays_eq;
29+
use crate::dtype::DType;
30+
use crate::dtype::Nullability;
31+
use crate::dtype::PType;
32+
use crate::expr::stats::Precision;
33+
use crate::expr::stats::Stat;
34+
use crate::expr::stats::StatsProvider;
35+
use crate::scalar::Scalar;
36+
use crate::serde::SerializeOptions;
37+
use crate::serde::SerializedArray;
38+
use crate::session::ArraySession;
39+
use crate::session::ArraySessionExt;
40+
use crate::validity::Validity;
41+
42+
fn serialize_blob(
43+
array: &ArrayRef,
44+
ctx: &ArrayContext,
45+
session: &VortexSession,
46+
) -> VortexResult<ByteBuffer> {
47+
let mut blob = ByteBufferMut::empty();
48+
for buffer in array.serialize(ctx, session, &SerializeOptions::default())? {
49+
blob.extend_from_slice(buffer.as_ref());
50+
}
51+
Ok(blob.freeze())
52+
}
53+
54+
/// Returns a `Widen` of the logical dtype `logical` over `values`, with the nullability of
55+
/// `values`.
56+
fn widen_over(values: impl IntoArray, logical: DType) -> VortexResult<ArrayRef> {
57+
let values = values.into_array();
58+
let logical = logical.with_nullability(values.dtype().nullability());
59+
Ok(WidenArray::try_new(values, logical)?.into_array())
60+
}
61+
62+
/// Serializes `array` with an unrestricted context and decodes it again. Returns the decoded
63+
/// array and the IDs that the context interned.
64+
fn serialize_round_trip(
65+
array: &ArrayRef,
66+
session: &VortexSession,
67+
) -> VortexResult<(ArrayRef, Vec<ArrayId>)> {
68+
let ctx = ArrayContext::empty();
69+
let serialized = SerializedArray::try_from(serialize_blob(array, &ctx, session)?)?;
70+
let ids = ctx.to_ids();
71+
let decoded = serialized.decode(
72+
array.dtype(),
73+
array.len(),
74+
&ReadContext::new(ids.clone()),
75+
session,
76+
)?;
77+
78+
Ok((decoded, ids))
79+
}
80+
81+
/// Asserts that `decoded` is a canonical leaf at the canonical width of its dtype.
82+
#[track_caller]
83+
fn assert_canonical_leaf(decoded: &ArrayRef) {
84+
assert!(decoded.is::<Primitive>(), "{}", decoded.encoding_id());
85+
}
86+
87+
#[rstest]
88+
#[case::integer(widen_over(buffer![-1i8, 0, 100], DType::from(PType::I64)))]
89+
#[case::integer_nullable(widen_over(
90+
PrimitiveArray::from_option_iter([Some(-300i16), None, Some(300)]),
91+
DType::from(PType::I32),
92+
))]
93+
#[case::empty(widen_over(
94+
PrimitiveArray::from_iter(Vec::<i16>::new()),
95+
DType::from(PType::I64),
96+
))]
97+
#[case::all_null(widen_over(
98+
PrimitiveArray::new(buffer![i8::MIN, i8::MAX], Validity::AllInvalid),
99+
DType::from(PType::I64),
100+
))]
101+
fn serialization_writes_widen(#[case] widen: VortexResult<ArrayRef>) -> VortexResult<()> {
102+
let widen = widen?;
103+
assert!(widen.is::<Widen>());
104+
105+
let (decoded, ids) = serialize_round_trip(&widen, &SESSION)?;
106+
107+
assert_canonical_leaf(&decoded);
108+
assert!(!ids.contains(&ArrayVTable::id(&Widen)), "{ids:?}");
109+
assert_arrays_eq!(decoded, widen, &mut SESSION.create_execution_ctx());
110+
111+
Ok(())
112+
}
113+
114+
#[test]
115+
fn serialization_writes_nested_widen() -> VortexResult<()> {
116+
let integer = widen_over(
117+
PrimitiveArray::from_option_iter([Some(1i8), None, Some(-7)]),
118+
DType::from(PType::I64),
119+
)?;
120+
let nested = StructArray::from_fields(&[("integer", integer.clone())])?.into_array();
121+
let chunked = ChunkedArray::try_new(
122+
vec![
123+
integer,
124+
PrimitiveArray::from_option_iter([Some(1000i64)]).into_array(),
125+
],
126+
DType::Primitive(PType::I64, Nullability::Nullable),
127+
)?
128+
.into_array();
129+
130+
for array in [nested, chunked] {
131+
let (decoded, ids) = serialize_round_trip(&array, &SESSION)?;
132+
assert!(!ids.contains(&ArrayVTable::id(&Widen)), "{ids:?}");
133+
assert_arrays_eq!(decoded, array, &mut SESSION.create_execution_ctx());
134+
}
135+
136+
Ok(())
137+
}
138+
139+
#[test]
140+
fn serialization_keeps_widen_statistics() -> VortexResult<()> {
141+
let widen = widen_over(buffer![-3i8, 5, 1], DType::from(PType::I64))?;
142+
let min = Scalar::primitive(-3i64, Nullability::NonNullable);
143+
let max = Scalar::primitive(5i64, Nullability::NonNullable);
144+
145+
for (stat, value) in [(Stat::Min, &min), (Stat::Max, &max)] {
146+
let value = value.value().cloned();
147+
let value = value.vortex_expect("the planted statistic is not null");
148+
widen.statistics().set(stat, Precision::Exact(value));
149+
}
150+
151+
let (decoded, _) = serialize_round_trip(&widen, &SESSION)?;
152+
153+
assert_eq!(decoded.statistics().get(Stat::Min), Precision::Exact(min));
154+
assert_eq!(decoded.statistics().get(Stat::Max), Precision::Exact(max));
155+
156+
Ok(())
157+
}
158+
159+
#[test]
160+
fn serialization_writes_widen_with_restricted_context() -> VortexResult<()> {
161+
let ctx = ArrayContext::new(vec![ArrayVTable::id(&Primitive)])
162+
.with_allowed_ids([ArrayVTable::id(&Primitive)].into_iter().collect());
163+
let widen = widen_over(buffer![-1i8, 0, 100], DType::from(PType::I64))?;
164+
165+
let serialized = SerializedArray::try_from(serialize_blob(&widen, &ctx, &SESSION)?)?;
166+
let read_ctx = ReadContext::new(ctx.to_ids());
167+
let decoded = serialized.decode(widen.dtype(), widen.len(), &read_ctx, &SESSION)?;
168+
169+
assert!(decoded.is::<Primitive>());
170+
assert_arrays_eq!(decoded, widen, &mut SESSION.create_execution_ctx());
171+
172+
Ok(())
173+
}
174+
175+
#[test]
176+
fn empty_session_can_register_widen_writer() -> VortexResult<()> {
177+
let session = VortexSession::empty().with_some(ArraySession::empty());
178+
session.arrays().register(Primitive);
179+
session.arrays().register(Bool);
180+
session.arrays().register(WidenPlugin);
181+
let widen = widen_over(buffer![-1i8, 0, 100], DType::from(PType::I64))?;
182+
183+
let (decoded, ids) = serialize_round_trip(&widen, &session)?;
184+
185+
assert!(
186+
!session
187+
.arrays()
188+
.registry()
189+
.contains_key(&ArrayVTable::id(&Widen))
190+
);
191+
assert!(!ids.contains(&ArrayVTable::id(&Widen)));
192+
assert_arrays_eq!(decoded, widen, &mut session.create_execution_ctx());
193+
194+
Ok(())
195+
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
mod operations;
5+
6+
mod plugin;
7+
pub use plugin::WidenPlugin;
58
mod validity;
69

710
use vortex_error::VortexResult;
@@ -91,8 +94,8 @@ impl VTable for Widen {
9194
_array: ArrayView<'_, Self>,
9295
_session: &VortexSession,
9396
) -> VortexResult<Option<Vec<u8>>> {
94-
// `ArraySession` does not register `Widen`, so `array_serialize` fails before it calls
95-
// this method. Only a session that registers `Widen` itself can reach it.
97+
// The default session registers `WidenPlugin`, which writes a storable representation
98+
// without calling this method. Direct registration of the vtable has no wire form.
9699
vortex_panic!("TODO(connor)[Widen]: Widen has no wire form yet (#10733)")
97100
}
98101

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Serialization of `Widen` arrays through existing wire forms.
5+
//!
6+
//! `Widen` has no wire form of its own, so its plugin registers only a writer.
7+
8+
use vortex_error::VortexResult;
9+
use vortex_error::vortex_bail;
10+
use vortex_session::VortexSession;
11+
12+
use crate::ArrayDeserialization;
13+
use crate::ArrayId;
14+
use crate::ArrayPlugin;
15+
use crate::ArrayRef;
16+
use crate::ArraySerialization;
17+
use crate::ArrayVTable;
18+
use crate::IntoArray;
19+
use crate::RecursiveCanonical;
20+
use crate::VortexSessionExecute;
21+
use crate::arrays::Primitive;
22+
use crate::arrays::Widen;
23+
use crate::session::ArraySessionExt;
24+
25+
/// Writes [`Widen`] arrays using an existing encoding's wire form.
26+
#[derive(Clone, Debug)]
27+
pub struct WidenPlugin;
28+
29+
impl ArrayPlugin for WidenPlugin {
30+
fn id(&self) -> ArrayId {
31+
ArrayVTable::id(&Widen)
32+
}
33+
34+
fn read_ids(&self) -> Vec<ArrayId> {
35+
vec![]
36+
}
37+
38+
fn write_ids(&self) -> Vec<ArrayId> {
39+
vec![ArrayVTable::id(&Primitive)]
40+
}
41+
42+
fn serialize(
43+
&self,
44+
array: &ArrayRef,
45+
session: &VortexSession,
46+
) -> VortexResult<Option<ArraySerialization>> {
47+
// TODO(connor)[Widen]: Add `vortex.widen` to both ID lists when it gets a wire form
48+
// (#10733), and write it only where the writer permits it. This method cannot see the
49+
// permitted IDs today.
50+
51+
// The child validity can be lazy, so the writer needs recursive canonicalization to
52+
// produce nodes that it can store.
53+
let canonical = array
54+
.clone()
55+
.execute::<RecursiveCanonical>(&mut session.create_execution_ctx())?
56+
.0
57+
.into_array();
58+
59+
session.array_serialize(&canonical)
60+
}
61+
62+
fn deserialize(
63+
&self,
64+
_parts: ArrayDeserialization<'_>,
65+
_session: &VortexSession,
66+
) -> VortexResult<ArrayRef> {
67+
vortex_bail!("Widen has no wire form yet (#10733)")
68+
}
69+
}

‎vortex-array/src/session/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::arrays::Union;
3838
use crate::arrays::VarBin;
3939
use crate::arrays::VarBinView;
4040
use crate::arrays::Variant;
41+
use crate::arrays::widen::WidenPlugin;
4142

4243
/// Registry of array encodings.
4344
pub type ArrayRegistry = ArcSwapMap<Id, ArrayPluginRef>;
@@ -109,6 +110,7 @@ impl Default for ArraySession {
109110
this.register(PiecewiseSequence);
110111
this.register(RunEnd);
111112
this.register(VarBin);
113+
this.register(WidenPlugin);
112114

113115
this
114116
}

‎vortex-ipc/src/messages/decoder.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,21 @@ impl MessageDecoder {
170170
mod test {
171171
use bytes::BytesMut;
172172
use vortex_array::ArrayRef;
173+
use vortex_array::ArrayVTable;
173174
use vortex_array::IntoArray;
174175
use vortex_array::VortexSessionExecute;
175176
use vortex_array::arrays::ConstantArray;
177+
use vortex_array::arrays::Primitive;
178+
use vortex_array::arrays::PrimitiveArray;
179+
use vortex_array::arrays::Widen;
180+
use vortex_array::arrays::WidenArray;
176181
use vortex_array::assert_arrays_eq;
182+
use vortex_array::dtype::DType;
183+
use vortex_array::dtype::Nullability;
184+
use vortex_array::dtype::PType;
185+
use vortex_array::validity::Validity;
177186
use vortex_buffer::buffer;
187+
use vortex_error::VortexResult;
178188
use vortex_error::vortex_panic;
179189

180190
use super::*;
@@ -220,4 +230,36 @@ mod test {
220230
assert_eq!(array.nbuffers(), 1, "Array should have a single buffer");
221231
write_and_read(&array.into_array());
222232
}
233+
234+
/// IPC writes a `Widen` as its canonical array, so the decoded encoding differs from the
235+
/// written one and this test cannot use `write_and_read`.
236+
#[test]
237+
fn array_ipc_writes_widen() -> VortexResult<()> {
238+
let values = PrimitiveArray::new(
239+
buffer![-125i16, 50, 900],
240+
Validity::from_iter([true, false, true]),
241+
);
242+
let dtype = DType::Primitive(PType::I64, Nullability::Nullable);
243+
let expected = WidenArray::try_new(values.into_array(), dtype)?.into_array();
244+
245+
let mut ipc_bytes = BytesMut::new();
246+
let mut encoder = MessageEncoder::new(SESSION.clone());
247+
for buf in encoder.encode(EncoderMessage::Array(&expected))? {
248+
ipc_bytes.extend_from_slice(buf.as_ref());
249+
}
250+
251+
let mut buffer = BytesMut::from(ipc_bytes.as_ref());
252+
let (array_parts, ctx, row_count) =
253+
match MessageDecoder::default().read_next(&mut buffer)? {
254+
PollRead::Some(DecoderMessage::Array(array_parts)) => array_parts,
255+
otherwise => vortex_panic!("Expected an array, got {:?}", otherwise),
256+
};
257+
let actual = array_parts.decode(expected.dtype(), row_count, &ctx, &SESSION)?;
258+
259+
assert!(!ctx.ids().contains(&ArrayVTable::id(&Widen)));
260+
assert!(actual.is::<Primitive>(), "{}", actual.encoding_id());
261+
assert_arrays_eq!(actual, expected, &mut SESSION.create_execution_ctx());
262+
263+
Ok(())
264+
}
223265
}

0 commit comments

Comments
 (0)