Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ num_enum = { version = "0.7.3", default-features = false }
object_store = { version = "0.13.2", default-features = false }
object_store_opendal = "0.58.0"
once_cell = "1.21.4"
onpair = "0.2.0"
onpair = "0.2.1"
opendal = { version = "0.58.1", default-features = false }
opentelemetry = "0.32.0"
opentelemetry-otlp = "0.32.0"
Expand Down
80 changes: 48 additions & 32 deletions encodings/onpair/src/compress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Train + compress entry points for the OnPair encoding.

use onpair::Config;
use onpair::Rows;
use vortex_array::ArrayRef;
use vortex_array::ExecutionCtx;
use vortex_array::IntoArray;
Expand All @@ -19,7 +20,6 @@ use vortex_buffer::ByteBuffer;
use vortex_buffer::ByteBufferMut;
use vortex_error::VortexExpect;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use vortex_mask::AllOr;

use crate::OnPair;
Expand All @@ -43,14 +43,8 @@ pub fn onpair_compress(
}

let views = array.views();
let flat_bytes: usize = views.iter().map(|v| v.len() as usize).sum();

// TODO(francesco): we flatten because onpair training needs a contiguous `(bytes, offsets)`
// pair. Allowing onpair to train on a slice-of-slices would let us skip this copy.
let mut flat: Vec<u8> = Vec::with_capacity(flat_bytes);
let mut offsets: Vec<u64> = Vec::with_capacity(len + 1);
let mut uncompressed_lengths: BufferMut<i32> = BufferMut::with_capacity(len);
offsets.push(0);
let mut uncompressed_lengths: BufferMut<u32> = BufferMut::zeroed(len);
let mut total_bytes = 0usize;
let buffers = array
.data_buffers()
.as_ref()
Expand All @@ -60,33 +54,33 @@ pub fn onpair_compress(

match mask.bit_buffer() {
AllOr::All => {
for view in views {
let bytes = view_bytes(view, &buffers);
flat.extend_from_slice(bytes);
offsets.push(u64::try_from(flat.len()).vortex_expect("offset must fit in u64"));
uncompressed_lengths
.push(i32::try_from(view.len()).vortex_expect("must fit in i32"));
for (view, length) in views.iter().zip(uncompressed_lengths.iter_mut()) {
*length = view.len();
total_bytes += *length as usize;
}
}
AllOr::None => unreachable!("all-null input handled above"),
AllOr::Some(validity) => {
for (view, valid) in views.iter().zip(validity.iter()) {
for ((view, length), valid) in views
.iter()
.zip(uncompressed_lengths.iter_mut())
.zip(validity.iter())
{
if valid {
let bytes = view_bytes(view, &buffers);
flat.extend_from_slice(bytes);
offsets.push(u64::try_from(flat.len()).vortex_expect("offset must fit in u64"));
uncompressed_lengths
.push(i32::try_from(view.len()).vortex_expect("must fit in i32"));
} else {
offsets.push(u64::try_from(flat.len()).vortex_expect("offset must fit in u64"));
uncompressed_lengths.push(0);
*length = view.len();
total_bytes += *length as usize;
}
}
}
}

let column = onpair::compress(&flat, &offsets, config)
.map_err(|e| vortex_err!("OnPair compress failed: {e}"))?;
let rows = ViewRows {
views,
buffers: &buffers,
lengths: uncompressed_lengths.as_slice(),
total_bytes,
};
let column = onpair::compress_rows::<_, u64>(&rows, config);
let (dict, codes, row_offsets) = column.into_raw();
let (dict_bytes, dict_offsets) = dict.into_raw();
let codes_offsets = codes_offsets_array(&row_offsets);
Expand All @@ -113,12 +107,34 @@ pub fn onpair_compress(
Ok(encoded.into_array())
}

fn view_bytes<'a>(view: &'a BinaryView, buffers: &'a [&ByteBuffer]) -> &'a [u8] {
if view.is_inlined() {
view.as_inlined().value()
} else {
let view_ref = view.as_view();
&buffers[view_ref.buffer_index as usize][view_ref.as_range()]
/// Reads inline and external values in place, treating null rows as empty.
struct ViewRows<'a> {
views: &'a [BinaryView],
buffers: &'a [&'a ByteBuffer],
lengths: &'a [u32],
total_bytes: usize,
}

impl Rows for ViewRows<'_> {
fn num_rows(&self) -> usize {
self.views.len()
}

fn total_bytes(&self) -> usize {
self.total_bytes
}

#[inline]
fn row(&self, i: usize) -> &[u8] {
let view = &self.views[i];
if self.lengths[i] == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if len is 0 the view should also be 0, I would personally skip this branch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lengths[i] is also zero for nulls, but masking can leave their views nonempty

&[]
} else if view.is_inlined() {
view.as_inlined().value()
} else {
let view_ref = view.as_view();
&self.buffers[view_ref.buffer_index as usize][view_ref.as_range()]
}
}
}

Expand Down
Loading