Skip to content

Commit 3934887

Browse files
committed
Added explicit encode/decode functions to the IndexVTable
Added some tests with golden files for serializing/deserializing index layouts.
1 parent 7a0b55e commit 3934887

10 files changed

Lines changed: 512 additions & 124 deletions

File tree

‎vortex-layout/examples/reverse_index/main.rs‎

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ use vortex_array::scalar_fn::fns::operators::Operator;
5252
use vortex_array::search_sorted::SearchSorted;
5353
use vortex_array::search_sorted::SearchSortedSide;
5454
use vortex_array::stream::ArrayStreamExt;
55-
use vortex_array::stream::SendableArrayStream;
5655
use vortex_array::validity::Validity;
5756
use vortex_buffer::ByteBufferMut;
5857
use vortex_edition::Edition;
@@ -117,10 +116,10 @@ impl ReverseIndex {
117116
}
118117
}
119118

120-
/// One decoded chunk of the index: its sorted keys and their serialized posting lists.
119+
/// A run of index entries: sorted, distinct, non-null keys and the rows holding each.
121120
pub struct Entries {
122121
keys: ArrayRef,
123-
postings: VarBinViewArray,
122+
postings: Vec<RoaringBitmap>,
124123
}
125124

126125
impl IndexVTable for ReverseIndex {
@@ -135,6 +134,10 @@ impl IndexVTable for ReverseIndex {
135134
*ID
136135
}
137136

137+
fn serialize_options(&self, _options: &()) -> Vec<u8> {
138+
vec![]
139+
}
140+
138141
fn deserialize_options(&self, _options: &[u8]) -> VortexResult<()> {
139142
Ok(())
140143
}
@@ -200,23 +203,57 @@ impl IndexVTable for ReverseIndex {
200203
}))
201204
}
202205

206+
fn encode(
207+
&self,
208+
chunk: &Entries,
209+
_options: &(),
210+
_ctx: &mut ExecutionCtx,
211+
) -> VortexResult<ArrayRef> {
212+
let mut lists = Vec::with_capacity(chunk.postings.len());
213+
for bitmap in &chunk.postings {
214+
let mut buffer = Vec::with_capacity(bitmap.serialized_size());
215+
bitmap
216+
.serialize_into(&mut buffer)
217+
.map_err(|err| vortex_err!("Failed to serialize postings: {err}"))?;
218+
lists.push(buffer);
219+
}
220+
221+
Ok(StructArray::try_new_with_dtype(
222+
vec![
223+
chunk.keys.clone(),
224+
VarBinViewArray::from_iter_bin(lists).into_array(),
225+
],
226+
index_fields(chunk.keys.dtype().clone()),
227+
chunk.keys.len(),
228+
Validity::NonNullable,
229+
)?
230+
.into_array())
231+
}
232+
203233
fn decode(
204234
&self,
205235
chunk: ArrayRef,
206236
_options: &(),
207237
ctx: &mut ExecutionCtx,
208238
) -> VortexResult<Entries> {
209239
let entries = chunk.execute::<StructArray>(ctx)?;
240+
let lists = entries
241+
.unmasked_field_by_name(POSTINGS_FIELD)?
242+
.clone()
243+
.execute::<VarBinViewArray>(ctx)?;
244+
let postings = (0..lists.len())
245+
.map(|idx| {
246+
RoaringBitmap::deserialize_from(lists.bytes_at(idx).as_slice())
247+
.map_err(|err| vortex_err!("Failed to deserialize postings: {err}"))
248+
})
249+
.collect::<VortexResult<_>>()?;
210250
Ok(Entries {
211251
keys: entries
212252
.unmasked_field_by_name(KEY_FIELD)?
213253
.clone()
214254
.execute::<Canonical>(ctx)?
215255
.into_array(),
216-
postings: entries
217-
.unmasked_field_by_name(POSTINGS_FIELD)?
218-
.clone()
219-
.execute::<VarBinViewArray>(ctx)?,
256+
postings,
220257
})
221258
}
222259

@@ -229,16 +266,13 @@ impl IndexVTable for ReverseIndex {
229266
) -> VortexResult<RowLocator> {
230267
// Keys are unique and sorted across the partition, so at most one chunk holds the target.
231268
for chunk in chunks {
232-
let Some(idx) = chunk
269+
if let Some(idx) = chunk
233270
.keys
234271
.search_sorted(target, SearchSortedSide::Left)?
235272
.to_found()
236-
else {
237-
continue;
238-
};
239-
let bitmap = RoaringBitmap::deserialize_from(chunk.postings.bytes_at(idx).as_slice())
240-
.map_err(|err| vortex_err!("Failed to deserialize postings: {err}"))?;
241-
return Ok(RowLocator::Rows(bitmap));
273+
{
274+
return Ok(RowLocator::Rows(chunk.postings[idx].clone()));
275+
}
242276
}
243277
Ok(RowLocator::empty_rows())
244278
}
@@ -258,6 +292,9 @@ pub struct Builder {
258292
}
259293

260294
impl IndexBuilder for Builder {
295+
type Options = ();
296+
type Chunk = Entries;
297+
261298
fn push(
262299
&mut self,
263300
chunk: &ArrayRef,
@@ -277,12 +314,12 @@ impl IndexBuilder for Builder {
277314
Ok(())
278315
}
279316

280-
fn finish(self: Box<Self>) -> VortexResult<Option<(SendableArrayStream, Vec<u8>)>> {
317+
fn finish(self) -> VortexResult<Option<(Vec<Entries>, ())>> {
281318
let Builder {
282319
dtype,
283320
postings,
284321
allocator,
285-
} = *self;
322+
} = self;
286323

287324
let mut entries: Vec<(Scalar, RoaringBitmap)> = postings.into_iter().collect();
288325
entries.sort_by(|(a, _), (b, _)| {
@@ -291,31 +328,22 @@ impl IndexBuilder for Builder {
291328
});
292329

293330
let key_dtype = dtype.as_nonnullable();
294-
let mut key_builder = builder_with_capacity_in(&key_dtype, entries.len(), &allocator);
295-
let mut lists = Vec::with_capacity(entries.len());
296-
for (key, bitmap) in &entries {
331+
let mut keys = builder_with_capacity_in(&key_dtype, entries.len(), &allocator);
332+
let mut bitmaps = Vec::with_capacity(entries.len());
333+
for (key, bitmap) in entries {
297334
// Keys are never null (`push` skips them), but may carry the source column's nullable
298335
// dtype; the key column itself is non-nullable, so normalize before appending.
299-
key_builder.append_scalar(&key.cast(&key_dtype)?)?;
300-
let mut buffer = Vec::with_capacity(bitmap.serialized_size());
301-
bitmap
302-
.serialize_into(&mut buffer)
303-
.map_err(|err| vortex_err!("Failed to serialize postings: {err}"))?;
304-
lists.push(buffer);
336+
keys.append_scalar(&key.cast(&key_dtype)?)?;
337+
bitmaps.push(bitmap);
305338
}
306339

307-
let len = entries.len();
308-
let array = StructArray::try_new_with_dtype(
309-
vec![
310-
key_builder.finish(),
311-
VarBinViewArray::from_iter_bin(lists).into_array(),
312-
],
313-
index_fields(key_dtype),
314-
len,
315-
Validity::NonNullable,
316-
)?;
317-
318-
Ok(Some((array.into_array().to_array_stream().boxed(), vec![])))
340+
Ok(Some((
341+
vec![Entries {
342+
keys: keys.finish(),
343+
postings: bitmaps,
344+
}],
345+
(),
346+
)))
319347
}
320348

321349
fn buffered_bytes(&self) -> u64 {

‎vortex-layout/examples/reverse_index/tests.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
//! End-to-end tests: write a column indexed by [`ReverseIndex`], then probe it.
22
3+
use std::io::Write;
34
use std::sync::Arc;
45

6+
use goldenfile::Mint;
7+
use roaring::RoaringBitmap;
8+
use rstest::rstest;
9+
510
use vortex_array::ArrayRef;
611
use vortex_array::IntoArray;
712
use vortex_array::MaskFuture;
813
use vortex_array::VortexSessionExecute;
914
use vortex_array::array_session;
15+
use vortex_array::assert_arrays_eq;
1016
use vortex_array::arrays::PrimitiveArray;
1117
use vortex_array::arrays::StructArray;
1218
use vortex_array::arrays::VarBinViewArray;
1319
use vortex_array::arrays::struct_::StructArrayExt;
20+
use vortex_array::dtype::DType;
1421
use vortex_array::dtype::FieldPath;
22+
use vortex_array::dtype::Nullability;
23+
use vortex_array::dtype::PType;
1524
use vortex_array::expr::col;
1625
use vortex_array::expr::eq;
1726
use vortex_array::expr::lit;
@@ -39,13 +48,15 @@ use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
3948
use vortex_layout::layouts::indexed::INDEXED_LAYOUT_ID;
4049
use vortex_layout::layouts::indexed::IndexConfig;
4150
use vortex_layout::layouts::indexed::IndexSessionExt;
51+
use vortex_layout::layouts::indexed::IndexVTable;
4252
use vortex_layout::layouts::indexed::IndexedStrategy;
4353
use vortex_layout::layouts::repartition::RepartitionStrategy;
4454
use vortex_layout::layouts::repartition::RepartitionWriterOptions;
4555
use vortex_layout::session::LayoutSession;
4656
use vortex_mask::Mask;
4757
use vortex_session::VortexSession;
4858

59+
use crate::Entries;
4960
use crate::ReverseIndex;
5061

5162
/// Small enough that a 12-row file spans three blocks, making the row/block granularity
@@ -348,3 +359,55 @@ async fn string_valued_column_is_indexed_exactly() -> VortexResult<()> {
348359
);
349360
Ok(())
350361
}
362+
363+
/// `ReverseIndex`'s content is a contract with every file already written: it must encode as the
364+
/// declared index dtype, and re-encoding its decoding must reproduce that encoding.
365+
///
366+
/// This is `vortex_layout::layouts::indexed::test_harness::check_roundtrip`, which kinds in other
367+
/// crates can call by enabling `vortex-layout`'s `_test-harness` feature; an example in the same
368+
/// package cannot enable it for itself. The goldenfile pins the encoded rows themselves, so a
369+
/// change to how keys or postings are laid out shows up as a diff against the checked-in file.
370+
#[rstest]
371+
#[case::i32(
372+
"reverse_index_i32.txt",
373+
PrimitiveArray::from_iter([10i32, 20, 30]).into_array(),
374+
DType::Primitive(PType::I32, Nullability::Nullable),
375+
)]
376+
#[case::utf8(
377+
"reverse_index_utf8.txt",
378+
VarBinViewArray::from_iter_str(["alpha", "beta", "gamma"]).into_array(),
379+
DType::Utf8(Nullability::NonNullable),
380+
)]
381+
fn reverse_index_roundtrips(
382+
#[case] golden: &str,
383+
#[case] keys: ArrayRef,
384+
#[case] data_dtype: DType,
385+
) -> VortexResult<()> {
386+
let mut ctx = session().create_execution_ctx();
387+
let entries = Entries {
388+
keys,
389+
postings: vec![
390+
RoaringBitmap::from_iter([0u32, 9]),
391+
RoaringBitmap::new(),
392+
RoaringBitmap::from_iter([3u32, 4, 100_000]),
393+
],
394+
};
395+
396+
let encoded = ReverseIndex.encode(&entries, &(), &mut ctx)?;
397+
assert_eq!(
398+
Some(encoded.dtype()),
399+
ReverseIndex.index_dtype(&data_dtype, &()).as_ref()
400+
);
401+
let decoded = ReverseIndex.decode(encoded.clone(), &(), &mut ctx)?;
402+
assert_arrays_eq!(
403+
ReverseIndex.encode(&decoded, &(), &mut ctx)?,
404+
encoded,
405+
&mut ctx
406+
);
407+
408+
let mut mint = Mint::new("goldenfiles/");
409+
let mut file = mint.new_goldenfile(golden)?;
410+
writeln!(file, "{}", encoded.display_values())?;
411+
Ok(())
412+
}
413+
86 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{key: 10i32, postings: "3a 30 0 0 1 0 0 0 0 0 1 0 10 0 0 0 0 0 9 0"}, {key: 20i32, postings: "3a 30 0 0 0 0 0 0"}, {key: 30i32, postings: "3a 30 0 0 2 0 0 0 0 0 1 0 1 0 0 0 18 0 0 0 1c 0 0 0 3 0 4 0 a0 86"}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{key: "alpha", postings: "3a 30 0 0 1 0 0 0 0 0 1 0 10 0 0 0 0 0 9 0"}, {key: "beta", postings: "3a 30 0 0 0 0 0 0"}, {key: "gamma", postings: "3a 30 0 0 2 0 0 0 0 0 1 0 1 0 0 0 18 0 0 0 1c 0 0 0 3 0 4 0 a0 86"}]

‎vortex-layout/src/layouts/indexed/index.rs‎

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use vortex_array::ArrayRef;
1111
use vortex_array::ExecutionCtx;
1212
use vortex_array::dtype::DType;
1313
use vortex_array::expr::BoundExpression;
14-
use vortex_array::stream::SendableArrayStream;
1514
use vortex_buffer::BitBufferMut;
1615
use vortex_error::VortexResult;
1716
use vortex_mask::Mask;
@@ -39,11 +38,25 @@ pub type IndexId = Id;
3938
/// [`Chunk`](Self::Chunk) at most once, sharing it between every expression that probes the
4039
/// chunk. Each expression is reduced by [`plan`](Self::plan) to a [`Query`](Self::Query) that
4140
/// [`resolve`](Self::resolve) answers against those decoded chunks.
41+
///
42+
/// # Serialization
43+
///
44+
/// A kind's on-disk format is exactly three things, and nothing else a kind does reaches the file:
45+
///
46+
/// - [`serialize_options`](Self::serialize_options) and
47+
/// [`deserialize_options`](Self::deserialize_options), for the options blob stored in the spec.
48+
/// The blob is the kind's to version.
49+
/// - [`index_dtype`](Self::index_dtype), the schema of the index child.
50+
/// - [`encode`](Self::encode) and [`decode`](Self::decode), between a [`Chunk`](Self::Chunk) and
51+
/// rows of that schema.
52+
///
53+
/// Builders produce chunks and options, never bytes, so these pairs can be tested without
54+
/// writing a layout. Re-encoding a decoded chunk must reproduce its encoding.
4255
pub trait IndexVTable: 'static + Send + Sync + Debug {
4356
/// The kind's options, parsed from the self-versioned options blob.
4457
type Options: 'static + Send + Sync;
4558
/// Builds the index on the write path.
46-
type Builder: IndexBuilder + 'static;
59+
type Builder: IndexBuilder<Options = Self::Options, Chunk = Self::Chunk> + 'static;
4760
/// What [`plan`](Self::plan) reduced one expression to, answered by
4861
/// [`resolve`](Self::resolve).
4962
type Query: 'static + Send + Sync;
@@ -53,6 +66,9 @@ pub trait IndexVTable: 'static + Send + Sync + Debug {
5366
/// Stable string id, e.g. `vortex.idx.reverse_index`.
5467
fn id(&self) -> IndexId;
5568

69+
/// Serialize options into the blob stored in the spec.
70+
fn serialize_options(&self, options: &Self::Options) -> Vec<u8>;
71+
5672
/// Parse an options blob, as configured on the write side or stored in a spec.
5773
fn deserialize_options(&self, options: &[u8]) -> VortexResult<Self::Options>;
5874

@@ -94,9 +110,20 @@ pub trait IndexVTable: 'static + Send + Sync + Debug {
94110
options: &Self::Options,
95111
) -> VortexResult<Option<IndexQueryPlan<Self::Query>>>;
96112

97-
/// Decode one chunk of the index child, in the index child's own schema.
113+
/// Encode a chunk a builder produced as rows of the index child, of the declared
114+
/// [`index_dtype`](Self::index_dtype).
115+
fn encode(
116+
&self,
117+
chunk: &Self::Chunk,
118+
options: &Self::Options,
119+
ctx: &mut ExecutionCtx,
120+
) -> VortexResult<ArrayRef>;
121+
122+
/// Decode rows of the index child, in its own schema, back into a chunk.
98123
///
99-
/// Called at most once per chunk per reader, however many expressions probe it.
124+
/// The index child's layout strategy may chunk it differently than it was encoded, so this
125+
/// must accept any contiguous run of encoded rows. Called at most once per chunk per reader,
126+
/// however many expressions probe it.
100127
fn decode(
101128
&self,
102129
chunk: ArrayRef,
@@ -190,6 +217,11 @@ impl<V: IndexVTable> DynIndexVTable for V {
190217

191218
/// Accumulates index content while the data stream is written.
192219
pub trait IndexBuilder: Send {
220+
/// The kind's options, as finally chosen by the build.
221+
type Options;
222+
/// The content the kind's [`IndexVTable::encode`] writes out.
223+
type Chunk;
224+
193225
/// Chunks arrive in stream order with their row offset within this builder's partition, which
194226
/// for an unpartitioned index is the whole layout.
195227
fn push(
@@ -199,17 +231,18 @@ pub trait IndexBuilder: Send {
199231
ctx: &mut ExecutionCtx,
200232
) -> VortexResult<()>;
201233

202-
/// Emit the index content as an array stream, to be written through a child layout strategy.
234+
/// Emit the index content as chunks, which the writer encodes and writes through a child
235+
/// layout strategy in order.
203236
///
204-
/// Returns the final serialized options alongside it, so builders can record normalization
205-
/// choices or block sizes discovered during the build.
237+
/// Returns the final options alongside them, so builders can record normalization choices or
238+
/// block sizes discovered during the build.
206239
///
207240
/// `None` declines: nothing worth keeping was built, so no index child and no spec are written,
208241
/// and the wrapper collapses to the plain data layout if every builder declines. This is the
209242
/// only point at which size can be judged — a builder is constructed before the first chunk
210243
/// arrives, so row count and cardinality are not knowable earlier. Declining is always safe: an
211244
/// absent index reads exactly like an unregistered one.
212-
fn finish(self: Box<Self>) -> VortexResult<Option<(SendableArrayStream, Vec<u8>)>>;
245+
fn finish(self) -> VortexResult<Option<(Vec<Self::Chunk>, Self::Options)>>;
213246

214247
/// Bytes currently buffered, reported up through the write context's buffered-bytes tracker.
215248
fn buffered_bytes(&self) -> u64;

0 commit comments

Comments
 (0)