mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 23:02:28 +00:00
fix: preserve nullable struct child offsets
This commit is contained in:
Generated
+1
-1
@@ -5217,7 +5217,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lance-arrow"
|
||||
version = "13.0.0-beta.8"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v13.0.0-beta.8#3c20b7875f621c929fdcecabe74dbca507ec0f0d"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5888,6 +5887,7 @@ dependencies = [
|
||||
"datafusion-common",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"lance-arrow",
|
||||
"lance-core",
|
||||
"lance-io",
|
||||
"lance-namespace",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[workspace]
|
||||
members = ["rust/lancedb", "nodejs", "python"]
|
||||
exclude = ["python/vendor/lance-arrow"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
@@ -79,6 +80,10 @@ tonic = { version = "0.14", features = ["tls-native-roots", "tls-ring"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "v7"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
|
||||
[patch."https://github.com/lance-format/lance.git"]
|
||||
# Carry the offset-safe validity fix until it is available in a Lance release.
|
||||
lance-arrow = { path = "python/vendor/lance-arrow" }
|
||||
|
||||
[profile.ci]
|
||||
debug = "line-tables-only"
|
||||
inherits = "dev"
|
||||
|
||||
@@ -22,6 +22,9 @@ bytes.workspace = true
|
||||
lancedb.workspace = true
|
||||
datafusion-common.workspace = true
|
||||
lance-core.workspace = true
|
||||
# Make the patched Lance Arrow crate an explicit path dependency so Maturin
|
||||
# includes it at the path used by the workspace patch in source distributions.
|
||||
lance-arrow = { version = "=13.0.0-beta.8", path = "vendor/lance-arrow" }
|
||||
lance-namespace.workspace = true
|
||||
lance-namespace-impls.workspace = true
|
||||
lance-io.workspace = true
|
||||
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "lance-arrow"
|
||||
version = "13.0.0-beta.8"
|
||||
edition = "2024"
|
||||
authors = ["Lance Devs <dev@lance.org>"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/lance-format/lance"
|
||||
readme = "README.md"
|
||||
description = "Arrow Extension for Lance"
|
||||
keywords = ["data-format", "data-science", "machine-learning", "apache-arrow", "data-analytics"]
|
||||
categories = ["database-implementations"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
arrow-array = "58.0.0"
|
||||
arrow-buffer = "58.0.0"
|
||||
arrow-data = "58.0.0"
|
||||
arrow-ipc = { version = "58.0.0", features = ["zstd"] }
|
||||
arrow-ord = "58.0.0"
|
||||
arrow-schema = "58.0.0"
|
||||
arrow-select = "58.0.0"
|
||||
bytemuck = { version = "1", default-features = false, features = ["extern_crate_alloc"] }
|
||||
bytes = "1.11.1"
|
||||
futures = "0.3"
|
||||
half = { version = "2.1", default-features = false, features = ["num-traits", "std", "bytemuck"] }
|
||||
jsonb = { version = "0.5.3", default-features = false, features = ["databend"] }
|
||||
num-traits = "0.2"
|
||||
rand = { version = "0.9.1", features = ["small_rng"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
This directory contains `rust/lance-arrow` from Lance tag `v13.0.0-beta.8`
|
||||
(commit `3c20b7875f621c929fdcecabe74dbca507ec0f0d`), licensed under Apache-2.0.
|
||||
|
||||
LanceDB carries a local fix in `src/lib.rs` for sliced child validity bitmaps
|
||||
until a Lance release includes the fix. The patch is selected in the root
|
||||
`Cargo.toml`; the rest of Lance stays on the pinned release. An explicit unsafe
|
||||
block in `src/bfloat16.rs` lets this copy pass LanceDB's Rust 2024 warning gate.
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
# lance-arrow
|
||||
|
||||
`lance-arrow` is an internal sub-crate, containing [Apache-Arrow](https://github.com/apache/arrow-rs)
|
||||
extensions used by [Lance](https://github.com/lance-format/lance).
|
||||
|
||||
**Important Note**: This crate is **not intended for external usage**.
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! bfloat16 support for Apache Arrow.
|
||||
|
||||
use std::fmt::Formatter;
|
||||
use std::slice;
|
||||
|
||||
use arrow_array::{Array, FixedSizeBinaryArray, builder::BooleanBufferBuilder};
|
||||
use arrow_buffer::{Buffer, MutableBuffer};
|
||||
use arrow_data::ArrayData;
|
||||
use arrow_schema::{ArrowError, DataType, Field as ArrowField};
|
||||
use half::bf16;
|
||||
|
||||
use crate::{ARROW_EXT_NAME_KEY, FloatArray};
|
||||
|
||||
/// The name of the bfloat16 extension in Arrow metadata
|
||||
pub const BFLOAT16_EXT_NAME: &str = "lance.bfloat16";
|
||||
|
||||
/// Check whether the given field is a bfloat16 field
|
||||
///
|
||||
/// A field is a bfloat16 field if it has a data type of `FixedSizeBinary(2)` and the metadata
|
||||
/// contains the bfloat16 extension name.
|
||||
pub fn is_bfloat16_field(field: &ArrowField) -> bool {
|
||||
field.data_type() == &DataType::FixedSizeBinary(2)
|
||||
&& field
|
||||
.metadata()
|
||||
.get(ARROW_EXT_NAME_KEY)
|
||||
.map(|name| name == BFLOAT16_EXT_NAME)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The bfloat16 data type
|
||||
///
|
||||
/// This implements the [`ArrowFloatType`](crate::floats::ArrowFloatType) trait for bfloat16 values.
|
||||
#[derive(Debug)]
|
||||
pub struct BFloat16Type {}
|
||||
|
||||
/// An array of bfloat16 values
|
||||
///
|
||||
/// Note that bfloat16 is not the same thing as fp16 which is supported natively by arrow-rs.
|
||||
#[derive(Clone)]
|
||||
pub struct BFloat16Array {
|
||||
inner: FixedSizeBinaryArray,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BFloat16Array {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BFloat16Array\n[\n")?;
|
||||
from_arrow::print_long_array(&self.inner, f, |array, i, f| {
|
||||
if array.is_null(i) {
|
||||
write!(f, "null")
|
||||
} else {
|
||||
let binary_values = array.value(i);
|
||||
let value =
|
||||
bf16::from_bits(u16::from_le_bytes([binary_values[0], binary_values[1]]));
|
||||
write!(f, "{:?}", value)
|
||||
}
|
||||
})?;
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
|
||||
impl BFloat16Array {
|
||||
pub fn from_iter_values(iter: impl IntoIterator<Item = bf16>) -> Self {
|
||||
let values: Vec<bf16> = iter.into_iter().collect();
|
||||
values.into()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_null(&self, i: usize) -> bool {
|
||||
self.inner.is_null(i)
|
||||
}
|
||||
|
||||
pub fn null_count(&self) -> usize {
|
||||
self.inner.null_count()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> BFloat16Iter<'_> {
|
||||
BFloat16Iter {
|
||||
array: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self, i: usize) -> bf16 {
|
||||
assert!(
|
||||
i < self.len(),
|
||||
"Trying to access an element at index {} from a BFloat16Array of length {}",
|
||||
i,
|
||||
self.len()
|
||||
);
|
||||
// Safety:
|
||||
// `i < self.len()
|
||||
unsafe { self.value_unchecked(i) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must ensure that `i < self.len()`
|
||||
pub unsafe fn value_unchecked(&self, i: usize) -> bf16 {
|
||||
// SAFETY: the caller guarantees that `i` is within this array's bounds.
|
||||
let binary_value = unsafe { self.inner.value_unchecked(i) };
|
||||
bf16::from_bits(u16::from_le_bytes([binary_value[0], binary_value[1]]))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> FixedSizeBinaryArray {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Option<bf16>> for BFloat16Array {
|
||||
fn from_iter<I: IntoIterator<Item = Option<bf16>>>(iter: I) -> Self {
|
||||
let mut buffer = MutableBuffer::new(10);
|
||||
// No null buffer builder :(
|
||||
let mut nulls = BooleanBufferBuilder::new(10);
|
||||
let mut len = 0;
|
||||
|
||||
for maybe_value in iter {
|
||||
if let Some(value) = maybe_value {
|
||||
let bytes = value.to_le_bytes();
|
||||
buffer.extend(bytes);
|
||||
} else {
|
||||
buffer.extend([0u8, 0u8]);
|
||||
}
|
||||
nulls.append(maybe_value.is_some());
|
||||
len += 1;
|
||||
}
|
||||
|
||||
let null_buffer = nulls.finish();
|
||||
let num_valid = null_buffer.count_set_bits();
|
||||
let null_buffer = if num_valid == len {
|
||||
None
|
||||
} else {
|
||||
Some(null_buffer.into_inner())
|
||||
};
|
||||
|
||||
let array_data = ArrayData::builder(DataType::FixedSizeBinary(2))
|
||||
.len(len)
|
||||
.add_buffer(buffer.into())
|
||||
.null_bit_buffer(null_buffer);
|
||||
// SAFETY: the value buffer contains exactly `2 * len` bytes (two bytes
|
||||
// pushed per iteration of the loop above, including the zero-fill for
|
||||
// null slots), which matches the `FixedSizeBinary(2)` storage layout.
|
||||
// The null bit buffer, when present, has `len` bits appended above, so
|
||||
// its length covers the array's logical range.
|
||||
let array_data = unsafe { array_data.build_unchecked() };
|
||||
Self {
|
||||
inner: FixedSizeBinaryArray::from(array_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<bf16> for BFloat16Array {
|
||||
fn from_iter<I: IntoIterator<Item = bf16>>(iter: I) -> Self {
|
||||
Self::from_iter_values(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<bf16>> for BFloat16Array {
|
||||
fn from(data: Vec<bf16>) -> Self {
|
||||
let len = data.len();
|
||||
// Zero-copy: `bf16` is `#[repr(transparent)]` over `u16` and derives
|
||||
// `bytemuck::Pod`, so `cast_vec` reinterprets the allocation in place —
|
||||
// no per-element copy or heap alloc. The crate-root `compile_error!`
|
||||
// pins `target_endian = "little"`, so the resulting bytes match the
|
||||
// `FixedSizeBinary(2)` on-disk order Lance writes elsewhere.
|
||||
let raw: Vec<u16> = bytemuck::cast_vec(data);
|
||||
let array_data = ArrayData::builder(DataType::FixedSizeBinary(2))
|
||||
.len(len)
|
||||
.add_buffer(Buffer::from_vec(raw));
|
||||
// SAFETY: the value buffer contains exactly `2 * len` bytes — one
|
||||
// `u16` per element after the layout-compatible cast — matching the
|
||||
// `FixedSizeBinary(2)` storage layout. No null buffer is attached, so
|
||||
// every element is logically valid.
|
||||
let array_data = unsafe { array_data.build_unchecked() };
|
||||
Self {
|
||||
inner: FixedSizeBinaryArray::from(array_data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FixedSizeBinaryArray> for BFloat16Array {
|
||||
type Error = ArrowError;
|
||||
|
||||
fn try_from(value: FixedSizeBinaryArray) -> Result<Self, Self::Error> {
|
||||
if value.value_length() == 2 {
|
||||
Ok(Self { inner: value })
|
||||
} else {
|
||||
Err(ArrowError::InvalidArgumentError(
|
||||
"FixedSizeBinaryArray must have a value length of 2".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for BFloat16Array {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner.eq(&other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BFloat16Iter<'a> {
|
||||
array: &'a BFloat16Array,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BFloat16Iter<'a> {
|
||||
type Item = Option<bf16>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index >= self.array.len() {
|
||||
return None;
|
||||
}
|
||||
let i = self.index;
|
||||
self.index += 1;
|
||||
if self.array.is_null(i) {
|
||||
Some(None)
|
||||
} else {
|
||||
Some(Some(self.array.value(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods that are lifted from arrow-rs temporarily until they are made public.
|
||||
mod from_arrow {
|
||||
use arrow_array::Array;
|
||||
|
||||
/// Helper function for printing potentially long arrays.
|
||||
pub(super) fn print_long_array<A, F>(
|
||||
array: &A,
|
||||
f: &mut std::fmt::Formatter,
|
||||
print_item: F,
|
||||
) -> std::fmt::Result
|
||||
where
|
||||
A: Array,
|
||||
F: Fn(&A, usize, &mut std::fmt::Formatter) -> std::fmt::Result,
|
||||
{
|
||||
let head = std::cmp::min(10, array.len());
|
||||
|
||||
for i in 0..head {
|
||||
if array.is_null(i) {
|
||||
writeln!(f, " null,")?;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
print_item(array, i, f)?;
|
||||
writeln!(f, ",")?;
|
||||
}
|
||||
}
|
||||
if array.len() > 10 {
|
||||
if array.len() > 20 {
|
||||
writeln!(f, " ...{} elements...,", array.len() - 20)?;
|
||||
}
|
||||
|
||||
let tail = std::cmp::max(head, array.len() - 10);
|
||||
|
||||
for i in tail..array.len() {
|
||||
if array.is_null(i) {
|
||||
writeln!(f, " null,")?;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
print_item(array, i, f)?;
|
||||
writeln!(f, ",")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatArray<BFloat16Type> for FixedSizeBinaryArray {
|
||||
type FloatType = BFloat16Type;
|
||||
|
||||
/// Returns the underlying `bf16` values as a borrowed slice.
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
/// - `value_length()` must be 2 (the `FixedSizeBinary(2)` storage shape
|
||||
/// used by [`BFloat16Array`]). Asserted at entry.
|
||||
/// - The value buffer must be at least 2-byte aligned. Lance's in-tree
|
||||
/// constructors always satisfy this: value buffers are built either via
|
||||
/// `MutableBuffer` (aligned to arrow-buffer's `ALIGNMENT` constant, ≥32
|
||||
/// bytes) or via `Buffer::from_vec::<u16>` (aligned to `align_of::<u16>()`
|
||||
/// == 2); both meet `bf16`'s 2-byte requirement. Externally-built
|
||||
/// `FixedSizeBinaryArray`s arriving via FFI, IPC, or
|
||||
/// `Buffer::from_custom_allocation` are not required by arrow-rs to be
|
||||
/// aligned beyond a single byte; passing one to this method violates the
|
||||
/// precondition. A `debug_assert` below catches such inputs in debug and
|
||||
/// test builds.
|
||||
///
|
||||
/// # Endianness
|
||||
///
|
||||
/// `lance-arrow` is gated on `target_endian = "little"` at the crate root,
|
||||
/// so this method always returns values in the same byte order Lance writes
|
||||
/// (see [`BFloat16Array::value`] and the [`FromIterator`] impls).
|
||||
fn as_slice(&self) -> &[bf16] {
|
||||
assert_eq!(
|
||||
self.value_length(),
|
||||
2,
|
||||
"BFloat16 arrays must use FixedSizeBinary(2) storage"
|
||||
);
|
||||
debug_assert_eq!(
|
||||
(self.value_data().as_ptr() as usize) % std::mem::align_of::<bf16>(),
|
||||
0,
|
||||
"BFloat16 value buffer must be at least 2-byte aligned"
|
||||
);
|
||||
// SAFETY:
|
||||
// - The assert above pins `value_size == 2`, so `value_data().len() / 2`
|
||||
// equals the array's logical element count.
|
||||
// `FixedSizeBinaryArray::From<ArrayData>` constructs its value buffer
|
||||
// as `buffers[0].slice_with_length(offset * 2, len * 2)` (arrow-array
|
||||
// `fixed_size_binary_array.rs`), so `value_data()` already returns
|
||||
// the offset-adjusted slice. Do not replace `value_data()` with an
|
||||
// accessor that returns the un-sliced backing buffer.
|
||||
// - `bf16` is `#[repr(transparent)]` over `u16` (size 2, alignment 2);
|
||||
// every `u16` bit pattern is a valid `bf16`, so any byte content
|
||||
// yields a defined value — never UB.
|
||||
// - Alignment is the caller's responsibility per the precondition
|
||||
// documented above. The `debug_assert_eq!` immediately preceding this
|
||||
// block catches violations in debug and test builds only — release
|
||||
// builds rely on callers honoring the precondition. arrow-rs
|
||||
// declares `FixedSizeBinary(n)`'s
|
||||
// `BufferSpec::FixedWidth { alignment: align_of::<u8>() == 1 }`
|
||||
// (arrow-data `data.rs`), so arrow-rs alone does not guarantee
|
||||
// 2-byte alignment. Lance's in-tree construction paths build value
|
||||
// buffers via `MutableBuffer` (arrow-buffer `ALIGNMENT` constant,
|
||||
// ≥32 bytes) or `Buffer::from_vec::<u16>` (2-byte aligned), both of
|
||||
// which satisfy `bf16`'s 2-byte requirement.
|
||||
// - The returned slice borrows from `self`; the underlying ref-counted,
|
||||
// immutable Arrow buffer cannot be mutated or freed for the slice's
|
||||
// lifetime.
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self.value_data().as_ptr() as *const bf16,
|
||||
self.value_data().len() / 2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_values(values: Vec<bf16>) -> Self {
|
||||
BFloat16Array::from(values).into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basics() {
|
||||
let values: Vec<f32> = vec![1.0, 2.0, 3.0];
|
||||
let values: Vec<bf16> = values.iter().map(|v| bf16::from_f32(*v)).collect();
|
||||
|
||||
let array = BFloat16Array::from_iter_values(values.clone());
|
||||
let array2 = BFloat16Array::from(values.clone());
|
||||
assert_eq!(array, array2);
|
||||
assert_eq!(array.len(), 3);
|
||||
|
||||
// Pin the raw little-endian bytes emitted by `From<Vec<bf16>>` (rewritten to
|
||||
// reinterpret the Vec via `bytemuck::cast_vec`), so a layout/byte-order
|
||||
// regression is caught directly rather than only through Debug formatting.
|
||||
// bf16 is the high 16 bits of the f32: 1.0->0x3F80, 2.0->0x4000, 3.0->0x4040.
|
||||
let inner = array2.clone().into_inner();
|
||||
let raw_bytes: Vec<u8> = (0..inner.len())
|
||||
.flat_map(|i| inner.value(i).to_vec())
|
||||
.collect();
|
||||
assert_eq!(raw_bytes, vec![0x80, 0x3F, 0x00, 0x40, 0x40, 0x40]);
|
||||
|
||||
let expected_fmt = "BFloat16Array\n[\n 1.0,\n 2.0,\n 3.0,\n]";
|
||||
assert_eq!(expected_fmt, format!("{:?}", array));
|
||||
|
||||
for (expected, value) in values.iter().zip(array.iter()) {
|
||||
assert_eq!(Some(*expected), value);
|
||||
}
|
||||
|
||||
for (expected, value) in values.as_slice().iter().zip(array2.iter()) {
|
||||
assert_eq!(Some(*expected), value);
|
||||
}
|
||||
|
||||
let arrow_array = array.into_inner();
|
||||
assert_eq!(arrow_array.as_slice(), values.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nulls() {
|
||||
let values: Vec<Option<bf16>> =
|
||||
vec![Some(bf16::from_f32(1.0)), None, Some(bf16::from_f32(3.0))];
|
||||
let array = BFloat16Array::from_iter(values.clone());
|
||||
assert_eq!(array.len(), 3);
|
||||
assert_eq!(array.null_count(), 1);
|
||||
|
||||
let expected_fmt = "BFloat16Array\n[\n 1.0,\n null,\n 3.0,\n]";
|
||||
assert_eq!(expected_fmt, format!("{:?}", array));
|
||||
|
||||
for (expected, value) in values.iter().zip(array.iter()) {
|
||||
assert_eq!(*expected, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{Array, RecordBatch, make_array};
|
||||
use arrow_buffer::{BooleanBuffer, Buffer, NullBuffer};
|
||||
use arrow_data::{ArrayData, ArrayDataBuilder, transform::MutableArrayData};
|
||||
use arrow_schema::DataType;
|
||||
|
||||
pub fn deep_copy_buffer(buffer: &Buffer) -> Buffer {
|
||||
Buffer::from(buffer.as_slice())
|
||||
}
|
||||
|
||||
pub fn deep_copy_nulls(nulls: Option<&NullBuffer>) -> Option<NullBuffer> {
|
||||
let nulls = nulls?;
|
||||
let bit_buffer = deep_copy_buffer(nulls.inner().inner());
|
||||
// SAFETY: `null_count` is taken from the source `NullBuffer`, which already
|
||||
// upheld `NullBuffer::new_unchecked`'s invariant — the unset-bit count over
|
||||
// the logical bit slice `[bit_offset, bit_offset + bit_len)`. `NullBuffer::slice`
|
||||
// adjusts only `BooleanBuffer::bit_offset` / `bit_len` and never byte-advances
|
||||
// the inner `Buffer`, so `deep_copy_buffer` (which copies the source `Buffer`'s
|
||||
// `as_slice()` view from byte 0) reproduces the exact bit pattern at the same
|
||||
// bit offsets; the unset-bit count is therefore preserved. `BooleanBuffer::new`
|
||||
// panics (does not UB) if `bit_offset + bit_len > 8 * buffer.len()`, and the
|
||||
// copy has the same length, so that check still passes.
|
||||
Some(unsafe {
|
||||
NullBuffer::new_unchecked(
|
||||
BooleanBuffer::new(bit_buffer, nulls.offset(), nulls.len()),
|
||||
nulls.null_count(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deep_copy_array_data(data: &ArrayData) -> ArrayData {
|
||||
let data_type = data.data_type().clone();
|
||||
let len = data.len();
|
||||
let nulls = deep_copy_nulls(data.nulls());
|
||||
let offset = data.offset();
|
||||
let buffers = data
|
||||
.buffers()
|
||||
.iter()
|
||||
.map(deep_copy_buffer)
|
||||
.collect::<Vec<_>>();
|
||||
let child_data = data
|
||||
.child_data()
|
||||
.iter()
|
||||
.map(deep_copy_array_data)
|
||||
.collect::<Vec<_>>();
|
||||
// SAFETY: `build_unchecked` inherits `ArrayData::new_unchecked`'s contract —
|
||||
// `(data_type, len, offset, nulls, buffers, child_data)` must form a valid
|
||||
// Arrow array. This call reproduces `data` structurally: `data_type`, `len`,
|
||||
// and `offset` are forwarded unchanged; each buffer is replaced by a byte-
|
||||
// identical copy of its offset-applied `as_slice()` view (the output buffer
|
||||
// is `MutableBuffer`-allocated, at least as aligned as the source); `nulls`
|
||||
// is deep-copied with the same bit offset/length and unset-bit count (see
|
||||
// `deep_copy_nulls`); `child_data` is recursively cloned with the same
|
||||
// guarantee. Every value-level invariant the source upheld — UTF-8 validity,
|
||||
// monotonic offsets, in-bounds dictionary indices, run-end monotonicity,
|
||||
// struct child-length matching — therefore transfers to the copy. If the
|
||||
// source `ArrayData` was itself constructed via `new_unchecked` with an
|
||||
// invalid payload, this function faithfully reproduces that invalidity.
|
||||
unsafe {
|
||||
ArrayDataBuilder::new(data_type)
|
||||
.len(len)
|
||||
.nulls(nulls)
|
||||
.offset(offset)
|
||||
.buffers(buffers)
|
||||
.child_data(child_data)
|
||||
.build_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deep_copy_array(array: &dyn Array) -> Arc<dyn Array> {
|
||||
let data = array.to_data();
|
||||
let data = deep_copy_array_data(&data);
|
||||
make_array(data)
|
||||
}
|
||||
|
||||
pub fn deep_copy_batch(batch: &RecordBatch) -> crate::Result<RecordBatch> {
|
||||
let arrays = batch
|
||||
.columns()
|
||||
.iter()
|
||||
.map(|array| deep_copy_array(array))
|
||||
.collect::<Vec<_>>();
|
||||
RecordBatch::try_new(batch.schema(), arrays)
|
||||
}
|
||||
|
||||
/// Deep copy array data, extracting only the sliced portion using MutableArrayData
|
||||
/// This is the most efficient and correct way to copy just the sliced data
|
||||
pub fn deep_copy_array_data_sliced(data: &ArrayData) -> ArrayData {
|
||||
// Use MutableArrayData to efficiently copy just the slice
|
||||
let mut mutable = MutableArrayData::new(vec![data], false, data.len());
|
||||
|
||||
// Which index this takes depends on the layout, because arrow's extenders
|
||||
// disagree about who applies `ArrayData::offset()`. The bit-packed one adds
|
||||
// it to the raw values buffer itself, so an offset-applied index there
|
||||
// reads from twice the offset and runs off the end of the buffer. Every
|
||||
// other layout either reads through an already-offset-applied view or
|
||||
// forwards the index to children carrying their own offsets, and takes the
|
||||
// absolute index it has always been given.
|
||||
let start = match data.data_type() {
|
||||
DataType::Boolean => 0,
|
||||
_ => data.offset(),
|
||||
};
|
||||
mutable.extend(0, start, start + data.len());
|
||||
|
||||
// Freeze into immutable ArrayData
|
||||
mutable.freeze()
|
||||
}
|
||||
|
||||
/// Deep copy an array, extracting only the sliced portion using MutableArrayData
|
||||
pub fn deep_copy_array_sliced(array: &dyn Array) -> Arc<dyn Array> {
|
||||
let data = array.to_data();
|
||||
let data = deep_copy_array_data_sliced(&data);
|
||||
make_array(data)
|
||||
}
|
||||
|
||||
/// Deep copy a RecordBatch, extracting only the sliced portion using MutableArrayData
|
||||
pub fn deep_copy_batch_sliced(batch: &RecordBatch) -> crate::Result<RecordBatch> {
|
||||
let arrays = batch
|
||||
.columns()
|
||||
.iter()
|
||||
.map(|array| deep_copy_array_sliced(array))
|
||||
.collect::<Vec<_>>();
|
||||
RecordBatch::try_new(batch.schema(), arrays)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{Array, BooleanArray, Int32Array, RecordBatch, StringArray};
|
||||
use arrow_data::ArrayDataBuilder;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
|
||||
#[test]
|
||||
fn raw_sliced_fixed_size_list_data_keeps_its_child_offset() {
|
||||
// `ArrayData::slice` records the offset on the parent and leaves the
|
||||
// child whole -- a shape `FixedSizeListArray::slice` never produces,
|
||||
// but a legal one this public helper can be handed directly. Arrow's
|
||||
// extender forwards `start * size` to that unsliced child and adds
|
||||
// nothing, so the index it gets has to be the absolute one; this is
|
||||
// here to stop the boolean fix from being generalised over it.
|
||||
let child = Int32Array::from(vec![10, 11, 20, 21]).to_data();
|
||||
let field = Arc::new(Field::new_list_field(DataType::Int32, false));
|
||||
let data = ArrayDataBuilder::new(DataType::FixedSizeList(field, 2))
|
||||
.len(2)
|
||||
.add_child_data(child)
|
||||
.build()
|
||||
.unwrap();
|
||||
let sliced = data.slice(1, 1);
|
||||
|
||||
let copied = super::deep_copy_array_data_sliced(&sliced);
|
||||
let copied_child = Int32Array::from(copied.child_data()[0].clone());
|
||||
assert_eq!(copied_child.values().as_ref(), &[20, 21]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_sliced_struct_of_fixed_size_list_reaches_the_right_values() {
|
||||
// Slicing raw struct data pushes the window into the struct's children,
|
||||
// and a fixed-size-list child takes it as its own parent offset with
|
||||
// its values left whole. Two levels of offset, and the absolute index
|
||||
// has to remain right through both of them.
|
||||
let values = Int32Array::from(vec![10, 11, 20, 21, 30, 31]).to_data();
|
||||
let item = Arc::new(Field::new_list_field(DataType::Int32, false));
|
||||
let list = ArrayDataBuilder::new(DataType::FixedSizeList(item, 2))
|
||||
.len(3)
|
||||
.add_child_data(values)
|
||||
.build()
|
||||
.unwrap();
|
||||
let field = Arc::new(Field::new("l", list.data_type().clone(), false));
|
||||
let data = ArrayDataBuilder::new(DataType::Struct(vec![field].into()))
|
||||
.len(3)
|
||||
.add_child_data(list)
|
||||
.build()
|
||||
.unwrap();
|
||||
let sliced = data.slice(2, 1);
|
||||
|
||||
let copied = super::deep_copy_array_data_sliced(&sliced);
|
||||
let copied_values = Int32Array::from(copied.child_data()[0].child_data()[0].clone());
|
||||
assert_eq!(copied_values.values().as_ref(), &[30, 31]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_parent_offset_struct_keeps_the_selected_child_row() {
|
||||
// A struct whose children are whole and whose window is the parent
|
||||
// offset: arrow's struct extender forwards the index to those children
|
||||
// untouched, so it has to be the absolute one.
|
||||
let field = Arc::new(Field::new("a", DataType::Int32, false));
|
||||
let data = ArrayDataBuilder::new(DataType::Struct(vec![field].into()))
|
||||
.len(1)
|
||||
.offset(1)
|
||||
.add_child_data(Int32Array::from(vec![10, 20]).to_data())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let copied = super::deep_copy_array_data_sliced(&data);
|
||||
let copied_child = Int32Array::from(copied.child_data()[0].clone());
|
||||
assert_eq!(copied_child.values().as_ref(), &[20]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliced_boolean_deep_copy_reads_from_the_slice() {
|
||||
// A boolean slice keeps its `ArrayData::offset()` -- the buffer cannot
|
||||
// advance by a fraction of a byte -- so a copy that adds the offset on
|
||||
// top of what `MutableArrayData` already applies reads past the end of
|
||||
// the values buffer and panics.
|
||||
let array = BooleanArray::from(vec![true, false, true, false, true, false, true, false]);
|
||||
for (offset, len) in [(0usize, 8usize), (1, 7), (3, 5), (7, 1)] {
|
||||
let sliced = array.slice(offset, len);
|
||||
let copied = super::deep_copy_array_sliced(&sliced);
|
||||
let copied = copied.as_any().downcast_ref::<BooleanArray>().unwrap();
|
||||
let expected: Vec<bool> = (0..len).map(|i| sliced.value(i)).collect();
|
||||
let actual: Vec<bool> = (0..len).map(|i| copied.value(i)).collect();
|
||||
assert_eq!(actual, expected, "offset={offset} len={len}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sliced_boolean_deep_copy_keeps_its_nulls() {
|
||||
let array = BooleanArray::from(vec![
|
||||
Some(true),
|
||||
None,
|
||||
Some(false),
|
||||
Some(true),
|
||||
None,
|
||||
Some(false),
|
||||
]);
|
||||
let sliced = array.slice(1, 4);
|
||||
let copied = super::deep_copy_array_sliced(&sliced);
|
||||
let copied = copied.as_any().downcast_ref::<BooleanArray>().unwrap();
|
||||
let expected: Vec<Option<bool>> = (0..sliced.len())
|
||||
.map(|i| (!sliced.is_null(i)).then(|| sliced.value(i)))
|
||||
.collect();
|
||||
let actual: Vec<Option<bool>> = (0..copied.len())
|
||||
.map(|i| (!copied.is_null(i)).then(|| copied.value(i)))
|
||||
.collect();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_copy_sliced_array_with_nulls() {
|
||||
let array = Arc::new(Int32Array::from(vec![
|
||||
Some(1),
|
||||
None,
|
||||
Some(3),
|
||||
None,
|
||||
Some(5),
|
||||
]));
|
||||
let sliced_array = array.slice(1, 3);
|
||||
let copied_array = super::deep_copy_array(&sliced_array);
|
||||
assert_eq!(sliced_array.len(), copied_array.len());
|
||||
assert_eq!(sliced_array.nulls(), copied_array.nulls());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_copy_array_data_sliced() {
|
||||
let array = Int32Array::from((0..1000).collect::<Vec<i32>>());
|
||||
let sliced = array.slice(100, 10);
|
||||
|
||||
let sliced_data = sliced.to_data();
|
||||
let copied_data = super::deep_copy_array_data_sliced(&sliced_data);
|
||||
|
||||
assert_eq!(copied_data.len(), 10);
|
||||
assert_eq!(copied_data.offset(), 0);
|
||||
|
||||
// Verify data correctness
|
||||
let copied_array = Int32Array::from(copied_data);
|
||||
for i in 0..10 {
|
||||
assert_eq!(copied_array.value(i), 100 + i as i32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_copy_array_sliced() {
|
||||
let array = Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]));
|
||||
let sliced = array.slice(1, 3);
|
||||
|
||||
let copied = super::deep_copy_array_sliced(&sliced);
|
||||
|
||||
assert_eq!(copied.len(), 3);
|
||||
let copied_int = copied.as_any().downcast_ref::<Int32Array>().unwrap();
|
||||
assert_eq!(copied_int.value(0), 2);
|
||||
assert_eq!(copied_int.value(1), 3);
|
||||
assert_eq!(copied_int.value(2), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_copy_batch_sliced() {
|
||||
let schema = Arc::new(Schema::new(vec![
|
||||
Field::new("id", DataType::Int32, false),
|
||||
Field::new("name", DataType::Utf8, false),
|
||||
]));
|
||||
|
||||
let id_array = Arc::new(Int32Array::from((0..100).collect::<Vec<i32>>()));
|
||||
let name_array = Arc::new(StringArray::from(
|
||||
(0..100)
|
||||
.map(|i| format!("name_{}", i))
|
||||
.collect::<Vec<String>>(),
|
||||
));
|
||||
|
||||
let batch = RecordBatch::try_new(
|
||||
schema,
|
||||
vec![id_array as Arc<dyn Array>, name_array as Arc<dyn Array>],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let sliced = batch.slice(10, 5);
|
||||
let copied = super::deep_copy_batch_sliced(&sliced).unwrap();
|
||||
|
||||
assert_eq!(copied.num_rows(), 5);
|
||||
assert_eq!(copied.num_columns(), 2);
|
||||
|
||||
// Verify data correctness
|
||||
let id_col = copied
|
||||
.column(0)
|
||||
.as_any()
|
||||
.downcast_ref::<Int32Array>()
|
||||
.unwrap();
|
||||
let name_col = copied
|
||||
.column(1)
|
||||
.as_any()
|
||||
.downcast_ref::<StringArray>()
|
||||
.unwrap();
|
||||
|
||||
for i in 0..5 {
|
||||
assert_eq!(id_col.value(i), 10 + i as i32);
|
||||
assert_eq!(name_col.value(i), format!("name_{}", 10 + i));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deep_copy_array_sliced_with_nulls() {
|
||||
let array = Arc::new(Int32Array::from(vec![
|
||||
Some(1),
|
||||
None,
|
||||
Some(3),
|
||||
None,
|
||||
Some(5),
|
||||
]));
|
||||
let sliced = array.slice(1, 3); // [None, Some(3), None]
|
||||
|
||||
let copied = super::deep_copy_array_sliced(&sliced);
|
||||
|
||||
assert_eq!(copied.len(), 3);
|
||||
assert_eq!(copied.null_count(), 2); // Two nulls in the slice
|
||||
|
||||
let copied_int = copied.as_any().downcast_ref::<Int32Array>().unwrap();
|
||||
assert!(!copied_int.is_valid(0)); // None
|
||||
assert!(copied_int.is_valid(1)); // Some(3)
|
||||
assert!(!copied_int.is_valid(2)); // None
|
||||
assert_eq!(copied_int.value(1), 3);
|
||||
}
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Floats Array
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::iter::Sum;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fmt::Formatter,
|
||||
ops::{AddAssign, DivAssign},
|
||||
};
|
||||
|
||||
use arrow_array::{
|
||||
Array, FixedSizeBinaryArray, Float16Array, Float32Array, Float64Array,
|
||||
types::{Float16Type, Float32Type, Float64Type},
|
||||
};
|
||||
use arrow_schema::{DataType, Field};
|
||||
use half::{bf16, f16};
|
||||
use num_traits::{AsPrimitive, Bounded, Float, FromPrimitive};
|
||||
|
||||
use super::bfloat16::{BFloat16Array, BFloat16Type};
|
||||
use crate::Result;
|
||||
use crate::bfloat16::is_bfloat16_field;
|
||||
|
||||
/// Float data type.
|
||||
///
|
||||
/// This helps differentiate between the different float types,
|
||||
/// because bf16 is not officially supported [DataType] in arrow-rs.
|
||||
#[derive(Debug)]
|
||||
pub enum FloatType {
|
||||
BFloat16,
|
||||
Float16,
|
||||
Float32,
|
||||
Float64,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FloatType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BFloat16 => write!(f, "bfloat16"),
|
||||
Self::Float16 => write!(f, "float16"),
|
||||
Self::Float32 => write!(f, "float32"),
|
||||
Self::Float64 => write!(f, "float64"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert a [DataType] to a [FloatType]. To support bfloat16, always
|
||||
/// prefer using the `TryFrom<&Field>` implementation.
|
||||
impl TryFrom<&DataType> for FloatType {
|
||||
type Error = crate::ArrowError;
|
||||
|
||||
fn try_from(value: &DataType) -> Result<Self> {
|
||||
match *value {
|
||||
DataType::Float16 => Ok(Self::Float16),
|
||||
DataType::Float32 => Ok(Self::Float32),
|
||||
DataType::Float64 => Ok(Self::Float64),
|
||||
_ => Err(crate::ArrowError::InvalidArgumentError(format!(
|
||||
"{:?} is not a floating type",
|
||||
value
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Field> for FloatType {
|
||||
type Error = crate::ArrowError;
|
||||
|
||||
fn try_from(field: &Field) -> Result<Self> {
|
||||
match field.data_type() {
|
||||
DataType::FixedSizeBinary(2) if is_bfloat16_field(field) => Ok(Self::BFloat16),
|
||||
_ => Self::try_from(field.data_type()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for float types used in Lance indexes
|
||||
///
|
||||
/// This mimics the utilities provided by [`arrow_array::ArrowPrimitiveType`]
|
||||
/// but applies to all float types (including bfloat16)
|
||||
pub trait ArrowFloatType: Debug {
|
||||
type Native: FromPrimitive
|
||||
+ FloatToArrayType<ArrowType = Self>
|
||||
+ AsPrimitive<f32>
|
||||
+ Debug
|
||||
+ Display;
|
||||
|
||||
const FLOAT_TYPE: FloatType;
|
||||
const MIN: Self::Native;
|
||||
const MAX: Self::Native;
|
||||
|
||||
/// Arrow Float Array Type.
|
||||
type ArrayType: FloatArray<Self>;
|
||||
|
||||
/// Returns empty array of this type.
|
||||
fn empty_array() -> Self::ArrayType {
|
||||
<Self::ArrayType as FloatArray<Self>>::from_values(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to be implemented by native types that have a corresponding [`ArrowFloatType`]
|
||||
/// implementation.
|
||||
///
|
||||
/// This helps define what operations are supported by native floats and also helps convert
|
||||
/// from a native type back to the corresponding Arrow float type.
|
||||
pub trait FloatToArrayType:
|
||||
Float
|
||||
+ Bounded
|
||||
+ Sum
|
||||
+ AddAssign<Self>
|
||||
+ AsPrimitive<f64>
|
||||
+ AsPrimitive<f32>
|
||||
+ DivAssign
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Copy
|
||||
{
|
||||
/// The corresponding [`ArrowFloatType`] implementation for this native type
|
||||
type ArrowType: ArrowFloatType<Native = Self>;
|
||||
}
|
||||
|
||||
impl FloatToArrayType for bf16 {
|
||||
type ArrowType = BFloat16Type;
|
||||
}
|
||||
|
||||
impl FloatToArrayType for f16 {
|
||||
type ArrowType = Float16Type;
|
||||
}
|
||||
|
||||
impl FloatToArrayType for f32 {
|
||||
type ArrowType = Float32Type;
|
||||
}
|
||||
|
||||
impl FloatToArrayType for f64 {
|
||||
type ArrowType = Float64Type;
|
||||
}
|
||||
|
||||
impl ArrowFloatType for BFloat16Type {
|
||||
type Native = bf16;
|
||||
|
||||
const FLOAT_TYPE: FloatType = FloatType::BFloat16;
|
||||
const MIN: Self::Native = bf16::MIN;
|
||||
const MAX: Self::Native = bf16::MAX;
|
||||
|
||||
type ArrayType = FixedSizeBinaryArray;
|
||||
}
|
||||
|
||||
impl ArrowFloatType for Float16Type {
|
||||
type Native = f16;
|
||||
|
||||
const FLOAT_TYPE: FloatType = FloatType::Float16;
|
||||
const MIN: Self::Native = f16::MIN;
|
||||
const MAX: Self::Native = f16::MAX;
|
||||
|
||||
type ArrayType = Float16Array;
|
||||
}
|
||||
|
||||
impl ArrowFloatType for Float32Type {
|
||||
type Native = f32;
|
||||
|
||||
const FLOAT_TYPE: FloatType = FloatType::Float32;
|
||||
const MIN: Self::Native = f32::MIN;
|
||||
const MAX: Self::Native = f32::MAX;
|
||||
|
||||
type ArrayType = Float32Array;
|
||||
}
|
||||
|
||||
impl ArrowFloatType for Float64Type {
|
||||
type Native = f64;
|
||||
|
||||
const FLOAT_TYPE: FloatType = FloatType::Float64;
|
||||
const MIN: Self::Native = f64::MIN;
|
||||
const MAX: Self::Native = f64::MAX;
|
||||
|
||||
type ArrayType = Float64Array;
|
||||
}
|
||||
|
||||
/// [FloatArray] is a trait that is implemented by all float type arrays
|
||||
///
|
||||
/// This is similar to [`arrow_array::PrimitiveArray`] but applies to all float types (including bfloat16)
|
||||
/// and is implemented as a trait and not a struct
|
||||
pub trait FloatArray<T: ArrowFloatType + ?Sized>: Array + Clone + 'static {
|
||||
type FloatType: ArrowFloatType;
|
||||
|
||||
/// Returns a reference to the underlying data as a slice.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Implementations may panic if the array's storage shape does not match
|
||||
/// the expected element layout. In particular, the `bf16` impl panics if
|
||||
/// `value_length() != 2` (the `FixedSizeBinary(2)` shape required by
|
||||
/// `BFloat16Array`).
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
/// Implementations may impose additional invariants on the underlying
|
||||
/// buffer. The `bf16` impl requires the value buffer to be at least
|
||||
/// 2-byte aligned — satisfied automatically by every in-tree Lance
|
||||
/// constructor, but external callers passing externally-built arrays
|
||||
/// (FFI, IPC, `Buffer::from_custom_allocation`) must ensure alignment.
|
||||
/// See the impl's docstring for details.
|
||||
fn as_slice(&self) -> &[T::Native];
|
||||
|
||||
/// Construct an array from a vector of values.
|
||||
fn from_values(values: Vec<T::Native>) -> Self;
|
||||
|
||||
/// Construct an array from an iterator of values.
|
||||
fn from_iter_values(values: impl IntoIterator<Item = T::Native>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Self::from_values(values.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatArray<Float16Type> for Float16Array {
|
||||
type FloatType = Float16Type;
|
||||
|
||||
fn as_slice(&self) -> &[<Float16Type as ArrowFloatType>::Native] {
|
||||
self.values()
|
||||
}
|
||||
|
||||
fn from_values(values: Vec<<Float16Type as ArrowFloatType>::Native>) -> Self {
|
||||
Self::from(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatArray<Float32Type> for Float32Array {
|
||||
type FloatType = Float32Type;
|
||||
|
||||
fn as_slice(&self) -> &[<Float32Type as ArrowFloatType>::Native] {
|
||||
self.values()
|
||||
}
|
||||
|
||||
fn from_values(values: Vec<<Float32Type as ArrowFloatType>::Native>) -> Self {
|
||||
Self::from(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatArray<Float64Type> for Float64Array {
|
||||
type FloatType = Float64Type;
|
||||
|
||||
fn as_slice(&self) -> &[<Float64Type as ArrowFloatType>::Native] {
|
||||
self.values()
|
||||
}
|
||||
|
||||
fn from_values(values: Vec<<Float64Type as ArrowFloatType>::Native>) -> Self {
|
||||
Self::from(values)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a float32 array to another float array
|
||||
///
|
||||
/// This is used during queries as query vectors are always provided as float32 arrays
|
||||
/// and need to be converted to the appropriate float type for the index.
|
||||
pub fn coerce_float_vector(input: &Float32Array, float_type: FloatType) -> Result<Arc<dyn Array>> {
|
||||
match float_type {
|
||||
FloatType::BFloat16 => Ok(Arc::new(
|
||||
BFloat16Array::from_iter_values(input.values().iter().map(|v| bf16::from_f32(*v)))
|
||||
.into_inner(),
|
||||
)),
|
||||
FloatType::Float16 => Ok(Arc::new(Float16Array::from_iter_values(
|
||||
input.values().iter().map(|v| f16::from_f32(*v)),
|
||||
))),
|
||||
FloatType::Float32 => Ok(Arc::new(input.clone())),
|
||||
FloatType::Float64 => Ok(Arc::new(Float64Array::from_iter_values(
|
||||
input.values().iter().map(|v| *v as f64),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_coerce_float_vector_bfloat16() {
|
||||
let input = Float32Array::from(vec![1.0f32, 2.0, 3.0]);
|
||||
let array = coerce_float_vector(&input, FloatType::BFloat16).unwrap();
|
||||
|
||||
assert_eq!(array.data_type(), &DataType::FixedSizeBinary(2));
|
||||
|
||||
let fixed = array
|
||||
.as_any()
|
||||
.downcast_ref::<FixedSizeBinaryArray>()
|
||||
.unwrap();
|
||||
let expected: Vec<bf16> = input.values().iter().map(|v| bf16::from_f32(*v)).collect();
|
||||
assert_eq!(fixed.as_slice(), expected.as_slice());
|
||||
}
|
||||
}
|
||||
Vendored
+620
@@ -0,0 +1,620 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Zero-copy Arrow IPC stream read/write utilities.
|
||||
//!
|
||||
//! Provides helpers for serializing and deserializing [`RecordBatch`]es as
|
||||
//! self-delimiting Arrow IPC streams using synchronous [`Read`]/[`Write`] I/O.
|
||||
//!
|
||||
//! These are designed for embedding IPC streams inside larger binary formats
|
||||
//! (e.g. a cache entry that contains multiple IPC sections). Each stream is
|
||||
//! self-delimiting (schema + batches + EOS marker) and can be read back
|
||||
//! independently.
|
||||
//!
|
||||
//! # Zero-copy reads
|
||||
//!
|
||||
//! [`read_ipc_stream`] and [`read_ipc_stream_single`] take `&Bytes` and use
|
||||
//! [`Bytes::slice`] to produce each message buffer. Because `Bytes::slice`
|
||||
//! increments a reference count rather than copying, the resulting
|
||||
//! [`Buffer`]s — and the array data decoded from them by [`FileDecoder`] —
|
||||
//! are all backed by the same allocation as the input.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::RecordBatch;
|
||||
use arrow_buffer::Buffer;
|
||||
use arrow_ipc::convert::fb_to_schema;
|
||||
use arrow_ipc::reader::FileDecoder;
|
||||
use arrow_ipc::root_as_message;
|
||||
use arrow_ipc::writer::StreamWriter;
|
||||
use arrow_schema::ArrowError;
|
||||
use bytes::Bytes;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Length-prefixed byte utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Write `data` prefixed by its length as a little-endian `u64`.
|
||||
///
|
||||
/// Paired with [`read_len_prefixed_bytes`].
|
||||
pub fn write_len_prefixed_bytes(writer: &mut dyn Write, data: &[u8]) -> Result<(), ArrowError> {
|
||||
writer
|
||||
.write_all(&(data.len() as u64).to_le_bytes())
|
||||
.map_err(|e| ArrowError::IoError(e.to_string(), e))?;
|
||||
writer
|
||||
.write_all(data)
|
||||
.map_err(|e| ArrowError::IoError(e.to_string(), e))
|
||||
}
|
||||
|
||||
/// Read a byte slice written by [`write_len_prefixed_bytes`].
|
||||
///
|
||||
/// Reads an 8-byte little-endian length then exactly that many bytes.
|
||||
pub fn read_len_prefixed_bytes(reader: &mut dyn Read) -> Result<Vec<u8>, ArrowError> {
|
||||
let mut len_buf = [0u8; 8];
|
||||
reader
|
||||
.read_exact(&mut len_buf)
|
||||
.map_err(|e| ArrowError::IoError(e.to_string(), e))?;
|
||||
let len = u64::from_le_bytes(len_buf) as usize;
|
||||
let mut buf = vec![0u8; len];
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| ArrowError::IoError(e.to_string(), e))?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC stream utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 4-byte continuation marker used by modern Arrow IPC streams.
|
||||
const IPC_CONTINUATION: [u8; 4] = [0xff; 4];
|
||||
|
||||
/// Write `batch` as a single-batch Arrow IPC stream to `writer`.
|
||||
pub fn write_ipc_stream(batch: &RecordBatch, writer: &mut dyn Write) -> Result<(), ArrowError> {
|
||||
let mut sw = StreamWriter::try_new(&mut *writer, batch.schema_ref())?;
|
||||
sw.write(batch)?;
|
||||
sw.finish()
|
||||
}
|
||||
|
||||
/// Write all batches from `iter` as a single Arrow IPC stream to `writer`.
|
||||
///
|
||||
/// `iter` must yield at least one batch; the schema is inferred from the first
|
||||
/// batch. Returns `ArrowError::InvalidArgumentError` if the iterator is empty.
|
||||
/// If you need to write an empty stream (schema only, no rows), construct a
|
||||
/// `StreamWriter` directly.
|
||||
pub fn write_ipc_stream_batches<I>(iter: I, writer: &mut dyn Write) -> Result<(), ArrowError>
|
||||
where
|
||||
I: IntoIterator<Item = RecordBatch>,
|
||||
{
|
||||
let mut iter = iter.into_iter();
|
||||
let first = iter
|
||||
.next()
|
||||
.ok_or_else(|| ArrowError::InvalidArgumentError("no batches to serialize".into()))?;
|
||||
let mut sw = StreamWriter::try_new(&mut *writer, first.schema_ref())?;
|
||||
sw.write(&first)?;
|
||||
for batch in iter {
|
||||
sw.write(&batch)?;
|
||||
}
|
||||
sw.finish()
|
||||
}
|
||||
|
||||
/// Read one complete Arrow IPC stream message from `data` as a zero-copy [`Buffer`].
|
||||
///
|
||||
/// Parses the first message starting at byte 0 of `data`. Returns `None` on
|
||||
/// EOS (size field == 0) or empty input. The returned [`Buffer`] is backed by
|
||||
/// `data`'s allocation — no bytes are copied.
|
||||
///
|
||||
/// The caller should advance its position by `buf.len()` after each call.
|
||||
fn read_one_ipc_message(data: &Bytes) -> Result<Option<Buffer>, ArrowError> {
|
||||
let bytes = data.as_ref();
|
||||
|
||||
if bytes.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if bytes.len() < 4 {
|
||||
return Err(ArrowError::IoError(
|
||||
"IPC: truncated header".into(),
|
||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated IPC header"),
|
||||
));
|
||||
}
|
||||
|
||||
let has_continuation = bytes[..4] == IPC_CONTINUATION;
|
||||
let (size_bytes, prefix_len): ([u8; 4], usize) = if has_continuation {
|
||||
if bytes.len() < 8 {
|
||||
return Err(ArrowError::IoError(
|
||||
"IPC: truncated header after continuation".into(),
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"truncated after continuation",
|
||||
),
|
||||
));
|
||||
}
|
||||
(bytes[4..8].try_into().unwrap(), 8)
|
||||
} else {
|
||||
(bytes[..4].try_into().unwrap(), 4)
|
||||
};
|
||||
|
||||
let meta_size = u32::from_le_bytes(size_bytes) as usize;
|
||||
if meta_size == 0 {
|
||||
return Ok(None); // EOS
|
||||
}
|
||||
|
||||
let meta_end = prefix_len + meta_size;
|
||||
if bytes.len() < meta_end {
|
||||
return Err(ArrowError::IoError(
|
||||
"IPC: truncated metadata".into(),
|
||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated IPC metadata"),
|
||||
));
|
||||
}
|
||||
|
||||
let msg = root_as_message(&bytes[prefix_len..meta_end])
|
||||
.map_err(|e| ArrowError::ParseError(format!("IPC message parse error: {e}")))?;
|
||||
let body_len = msg.bodyLength() as usize;
|
||||
|
||||
let total = meta_end + body_len;
|
||||
if bytes.len() < total {
|
||||
return Err(ArrowError::IoError(
|
||||
"IPC: truncated body".into(),
|
||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated IPC body"),
|
||||
));
|
||||
}
|
||||
|
||||
// Zero-copy: Bytes::slice shares the backing allocation; Buffer::from
|
||||
// wraps it without copying.
|
||||
Ok(Some(Buffer::from(data.slice(0..total))))
|
||||
}
|
||||
|
||||
/// Read a length-prefixed byte slice at `offset` in `data`, advancing `offset`.
|
||||
///
|
||||
/// Reads an 8-byte little-endian length, then slices exactly that many bytes
|
||||
/// from `data`. The returned [`Bytes`] is zero-copy (shares `data`'s allocation).
|
||||
pub fn read_len_prefixed_bytes_at(data: &Bytes, offset: &mut usize) -> Result<Bytes, ArrowError> {
|
||||
let bytes = data.as_ref();
|
||||
let len_end = offset
|
||||
.checked_add(8)
|
||||
.filter(|&e| e <= bytes.len())
|
||||
.ok_or_else(|| {
|
||||
ArrowError::IoError(
|
||||
"length-prefixed bytes: truncated length field".into(),
|
||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated length"),
|
||||
)
|
||||
})?;
|
||||
let len = u64::from_le_bytes(bytes[*offset..len_end].try_into().unwrap()) as usize;
|
||||
*offset = len_end;
|
||||
let data_end = offset
|
||||
.checked_add(len)
|
||||
.filter(|&e| e <= bytes.len())
|
||||
.ok_or_else(|| {
|
||||
ArrowError::IoError(
|
||||
"length-prefixed bytes: truncated data".into(),
|
||||
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated data"),
|
||||
)
|
||||
})?;
|
||||
let result = data.slice(*offset..data_end);
|
||||
*offset = data_end;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Read all [`RecordBatch`]es from one Arrow IPC stream starting at `offset`,
|
||||
/// advancing `offset` past the stream (including the EOS marker).
|
||||
///
|
||||
/// Zero-copy: array buffers borrow from `data`'s allocation.
|
||||
pub fn read_ipc_stream_at(
|
||||
data: &Bytes,
|
||||
offset: &mut usize,
|
||||
) -> Result<Vec<RecordBatch>, ArrowError> {
|
||||
let batches = read_ipc_stream(&data.slice(*offset..))?;
|
||||
|
||||
// Recompute how many bytes were consumed by re-parsing message sizes.
|
||||
// We can't get this from read_ipc_stream directly, so we re-walk the
|
||||
// message headers (metadata only, no body re-read) to sum up lengths.
|
||||
let slice = &data.as_ref()[*offset..];
|
||||
let mut consumed = 0usize;
|
||||
loop {
|
||||
let rem = &slice[consumed..];
|
||||
if rem.is_empty() {
|
||||
break;
|
||||
}
|
||||
let has_cont = rem.len() >= 4 && rem[..4] == IPC_CONTINUATION;
|
||||
let (size_bytes, prefix_len): ([u8; 4], usize) = if has_cont {
|
||||
if rem.len() < 8 {
|
||||
break;
|
||||
}
|
||||
(rem[4..8].try_into().unwrap(), 8)
|
||||
} else {
|
||||
if rem.len() < 4 {
|
||||
break;
|
||||
}
|
||||
(rem[..4].try_into().unwrap(), 4)
|
||||
};
|
||||
let meta_size = u32::from_le_bytes(size_bytes) as usize;
|
||||
if meta_size == 0 {
|
||||
// EOS — consume it and stop.
|
||||
consumed += prefix_len;
|
||||
break;
|
||||
}
|
||||
let meta_end = prefix_len + meta_size;
|
||||
if rem.len() < meta_end {
|
||||
break;
|
||||
}
|
||||
let msg = root_as_message(&rem[prefix_len..meta_end])
|
||||
.map_err(|e| ArrowError::ParseError(format!("IPC message parse error: {e}")))?;
|
||||
let body_len = msg.bodyLength() as usize;
|
||||
consumed += meta_end + body_len;
|
||||
}
|
||||
*offset += consumed;
|
||||
|
||||
Ok(batches)
|
||||
}
|
||||
|
||||
/// Read exactly one [`RecordBatch`] from one Arrow IPC stream starting at `offset`,
|
||||
/// advancing `offset` past the stream (including the EOS marker).
|
||||
///
|
||||
/// Zero-copy: array buffers borrow from `data`'s allocation.
|
||||
pub fn read_ipc_stream_single_at(
|
||||
data: &Bytes,
|
||||
offset: &mut usize,
|
||||
) -> Result<RecordBatch, ArrowError> {
|
||||
let mut batches = read_ipc_stream_at(data, offset)?;
|
||||
match batches.len() {
|
||||
1 => Ok(batches.remove(0)),
|
||||
n => Err(ArrowError::ParseError(format!(
|
||||
"expected exactly 1 IPC record batch, got {n}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the prefix length and metadata size from a raw IPC message buffer.
|
||||
///
|
||||
/// Modern IPC streams have an 8-byte prefix `[continuation: 4][size: 4]`.
|
||||
/// Legacy streams have a 4-byte prefix `[size: 4]`. Returns `(prefix_len, meta_size)`.
|
||||
fn parse_ipc_message_prefix(buf: &Buffer) -> Result<(usize, usize), ArrowError> {
|
||||
let has_continuation = buf.len() >= 4 && buf[..4] == IPC_CONTINUATION;
|
||||
if has_continuation {
|
||||
if buf.len() < 8 {
|
||||
return Err(ArrowError::ParseError(
|
||||
"IPC message buffer too short".into(),
|
||||
));
|
||||
}
|
||||
let meta_size = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
|
||||
Ok((8, meta_size))
|
||||
} else {
|
||||
if buf.len() < 4 {
|
||||
return Err(ArrowError::ParseError(
|
||||
"IPC message buffer too short".into(),
|
||||
));
|
||||
}
|
||||
let meta_size = u32::from_le_bytes(buf[..4].try_into().unwrap()) as usize;
|
||||
Ok((4, meta_size))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all [`RecordBatch`]es from one Arrow IPC stream.
|
||||
///
|
||||
/// Zero-copy: each batch's array data buffers are borrowed from the input
|
||||
/// message buffer(s) and not copied during decoding.
|
||||
///
|
||||
/// Uses [`FileDecoder`] directly (rather than `StreamDecoder`) to avoid a
|
||||
/// known edge case where `StreamDecoder` does not produce a batch for messages
|
||||
/// with a zero-length body when the message exactly fills the decode buffer.
|
||||
pub fn read_ipc_stream(data: &Bytes) -> Result<Vec<RecordBatch>, ArrowError> {
|
||||
let mut offset = 0usize;
|
||||
|
||||
let schema_buf = read_one_ipc_message(&data.slice(offset..))?.ok_or_else(|| {
|
||||
ArrowError::ParseError("IPC stream: expected schema message, got EOS".into())
|
||||
})?;
|
||||
offset += schema_buf.len();
|
||||
|
||||
let (prefix_len, meta_size) = parse_ipc_message_prefix(&schema_buf)?;
|
||||
let schema_msg = root_as_message(&schema_buf[prefix_len..prefix_len + meta_size])
|
||||
.map_err(|e| ArrowError::ParseError(format!("IPC schema parse error: {e}")))?;
|
||||
let schema = Arc::new(fb_to_schema(schema_msg.header_as_schema().ok_or_else(
|
||||
|| ArrowError::ParseError("IPC stream: first message is not a schema".into()),
|
||||
)?));
|
||||
let mut decoder = FileDecoder::new(schema, schema_msg.version());
|
||||
|
||||
let mut batches = Vec::new();
|
||||
|
||||
loop {
|
||||
let Some(buf) = read_one_ipc_message(&data.slice(offset..))? else {
|
||||
break;
|
||||
};
|
||||
offset += buf.len();
|
||||
|
||||
let (prefix_len, meta_size) = parse_ipc_message_prefix(&buf)?;
|
||||
let msg = root_as_message(&buf[prefix_len..prefix_len + meta_size])
|
||||
.map_err(|e| ArrowError::ParseError(format!("IPC message parse error: {e}")))?;
|
||||
let body_len = msg.bodyLength() as usize;
|
||||
|
||||
// Block offset = 0 since the buffer starts at the message boundary.
|
||||
// metaDataLength = prefix_len + meta_size (prefix + flatbuf + padding).
|
||||
let block = arrow_ipc::Block::new(0, (prefix_len + meta_size) as i32, body_len as i64);
|
||||
|
||||
match msg.header_type() {
|
||||
arrow_ipc::MessageHeader::RecordBatch => {
|
||||
if let Some(batch) = decoder.read_record_batch(&block, &buf)? {
|
||||
batches.push(batch);
|
||||
}
|
||||
}
|
||||
arrow_ipc::MessageHeader::DictionaryBatch => {
|
||||
decoder.read_dictionary(&block, &buf)?;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(batches)
|
||||
}
|
||||
|
||||
/// Read exactly one [`RecordBatch`] from one Arrow IPC stream.
|
||||
pub fn read_ipc_stream_single(data: &Bytes) -> Result<RecordBatch, ArrowError> {
|
||||
let mut batches = read_ipc_stream(data)?;
|
||||
match batches.len() {
|
||||
1 => Ok(batches.remove(0)),
|
||||
n => Err(ArrowError::ParseError(format!(
|
||||
"expected exactly 1 IPC record batch, got {n}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Aligned IPC sections
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Byte alignment that each IPC section's stream start is padded to.
|
||||
///
|
||||
/// When several IPC streams are concatenated into one larger blob (e.g. a
|
||||
/// cache entry), a section that starts at an arbitrary offset would leave its
|
||||
/// array data misaligned. [`FileDecoder`] with `require_alignment = false`
|
||||
/// then silently copies each buffer into a freshly aligned allocation on
|
||||
/// every read, defeating zero-copy. Padding each section start to a 64-byte
|
||||
/// boundary keeps the decoded buffers borrowed directly from the input.
|
||||
pub const IPC_SECTION_ALIGNMENT: usize = 64;
|
||||
|
||||
/// Number of zero-padding bytes needed to advance `pos` to the next
|
||||
/// [`IPC_SECTION_ALIGNMENT`] boundary.
|
||||
fn section_padding(pos: usize) -> usize {
|
||||
(IPC_SECTION_ALIGNMENT - (pos % IPC_SECTION_ALIGNMENT)) % IPC_SECTION_ALIGNMENT
|
||||
}
|
||||
|
||||
/// A [`Write`] adapter that counts the bytes written through it.
|
||||
struct CountingWriter<'a> {
|
||||
inner: &'a mut dyn Write,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl Write for CountingWriter<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let n = self.inner.write(buf)?;
|
||||
self.count += n;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// Write zero padding so the next byte lands on an [`IPC_SECTION_ALIGNMENT`]
|
||||
/// boundary, advancing `pos` past it.
|
||||
fn write_section_padding(writer: &mut dyn Write, pos: &mut usize) -> Result<(), ArrowError> {
|
||||
let pad = section_padding(*pos);
|
||||
if pad > 0 {
|
||||
const ZEROS: [u8; IPC_SECTION_ALIGNMENT] = [0u8; IPC_SECTION_ALIGNMENT];
|
||||
writer
|
||||
.write_all(&ZEROS[..pad])
|
||||
.map_err(|e| ArrowError::IoError(e.to_string(), e))?;
|
||||
*pos += pad;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write `batch` as a 64-byte-aligned single-batch Arrow IPC section.
|
||||
///
|
||||
/// `pos` is the absolute byte offset of `writer` within the enclosing blob.
|
||||
/// Zero padding is written first so the IPC stream begins on an
|
||||
/// [`IPC_SECTION_ALIGNMENT`] boundary, then the stream itself. `pos` is
|
||||
/// advanced past both the padding and the stream so the caller can write
|
||||
/// further aligned sections.
|
||||
///
|
||||
/// Paired with [`read_ipc_section_at`]. For the decoded buffers to be borrowed
|
||||
/// zero-copy, the blob must ultimately be read back from a buffer whose base
|
||||
/// address is at least 64-byte aligned.
|
||||
pub fn write_ipc_section(
|
||||
writer: &mut dyn Write,
|
||||
pos: &mut usize,
|
||||
batch: &RecordBatch,
|
||||
) -> Result<(), ArrowError> {
|
||||
write_section_padding(writer, pos)?;
|
||||
|
||||
let mut counting = CountingWriter {
|
||||
inner: writer,
|
||||
count: 0,
|
||||
};
|
||||
write_ipc_stream(batch, &mut counting)?;
|
||||
*pos += counting.count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a single [`RecordBatch`] from an aligned IPC section at `offset`.
|
||||
///
|
||||
/// Skips the alignment padding written by [`write_ipc_section`], then reads
|
||||
/// the stream, advancing `offset` past the section (padding + stream + EOS).
|
||||
///
|
||||
/// Zero-copy: array buffers borrow from `data`'s allocation when `data`'s base
|
||||
/// address is at least 64-byte aligned (see [`write_ipc_section`]).
|
||||
pub fn read_ipc_section_at(data: &Bytes, offset: &mut usize) -> Result<RecordBatch, ArrowError> {
|
||||
*offset += section_padding(*offset);
|
||||
read_ipc_stream_single_at(data, offset)
|
||||
}
|
||||
|
||||
/// Write `batches` as a single 64-byte-aligned multi-batch Arrow IPC section.
|
||||
///
|
||||
/// Like [`write_ipc_section`] but emits every batch from `iter` into one IPC
|
||||
/// stream (schema + N batches + EOS). `iter` must yield at least one batch.
|
||||
/// Paired with [`read_ipc_section_batches_at`].
|
||||
pub fn write_ipc_section_batches<I>(
|
||||
writer: &mut dyn Write,
|
||||
pos: &mut usize,
|
||||
iter: I,
|
||||
) -> Result<(), ArrowError>
|
||||
where
|
||||
I: IntoIterator<Item = RecordBatch>,
|
||||
{
|
||||
write_section_padding(writer, pos)?;
|
||||
|
||||
let mut counting = CountingWriter {
|
||||
inner: writer,
|
||||
count: 0,
|
||||
};
|
||||
write_ipc_stream_batches(iter, &mut counting)?;
|
||||
*pos += counting.count;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read all [`RecordBatch`]es from an aligned multi-batch IPC section at
|
||||
/// `offset`, advancing `offset` past the section (padding + stream + EOS).
|
||||
///
|
||||
/// Zero-copy: array buffers borrow from `data`'s allocation when `data`'s base
|
||||
/// address is at least 64-byte aligned (see [`write_ipc_section_batches`]).
|
||||
pub fn read_ipc_section_batches_at(
|
||||
data: &Bytes,
|
||||
offset: &mut usize,
|
||||
) -> Result<Vec<RecordBatch>, ArrowError> {
|
||||
*offset += section_padding(*offset);
|
||||
read_ipc_stream_at(data, offset)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arrow_array::{ArrayRef, record_batch};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ipc_roundtrip() {
|
||||
let batch1 = record_batch!(
|
||||
("int", Int32, [1, 2, 3]),
|
||||
("str", Utf8, ["foo", "bar", "baz"])
|
||||
)
|
||||
.unwrap();
|
||||
let batch2 = record_batch!(("int", Int32, [4, 5]), ("str", Utf8, ["qux", "quux"])).unwrap();
|
||||
let batches = vec![batch1.clone(), batch2.clone()];
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_ipc_stream_batches(batches, &mut buf).unwrap();
|
||||
|
||||
let data = Bytes::from(buf);
|
||||
|
||||
let batches = read_ipc_stream(&data).unwrap();
|
||||
assert_eq!(batches.len(), 2);
|
||||
assert_eq!(batches[0], batch1);
|
||||
assert_eq!(batches[1], batch2);
|
||||
|
||||
let data_base = data.as_ptr() as usize;
|
||||
let data_end = data_base + data.len();
|
||||
let assert_col_zero_copy = |array: &ArrayRef| {
|
||||
for buffer in array.to_data().buffers() {
|
||||
let ptr = buffer.as_ptr() as usize;
|
||||
assert!(
|
||||
ptr >= data_base && ptr < data_end,
|
||||
"buffer at {ptr:#x} is not backed by the input Bytes allocation \
|
||||
[{data_base:#x}..{data_end:#x})"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for batch in &batches {
|
||||
assert_eq!(batch.schema(), batch1.schema());
|
||||
assert_col_zero_copy(batch.column(0));
|
||||
assert_col_zero_copy(batch.column(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate a [`Bytes`] whose base address is 64-byte aligned, modelling a
|
||||
/// backend that reads cache entries into an aligned buffer. A plain
|
||||
/// `Bytes::from(vec)` only guarantees the allocator's alignment for `u8`.
|
||||
fn aligned_bytes(payload: &[u8]) -> Bytes {
|
||||
let mut v = vec![0u8; payload.len() + IPC_SECTION_ALIGNMENT];
|
||||
let pad = section_padding(v.as_ptr() as usize);
|
||||
v[pad..pad + payload.len()].copy_from_slice(payload);
|
||||
Bytes::from(v).slice(pad..pad + payload.len())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aligned_ipc_sections_are_zero_copy() {
|
||||
// A LargeBinary column exercises the i64-offset buffer whose 8-byte
|
||||
// alignment requirement triggers a realigning memcpy when misaligned.
|
||||
let blocks = arrow_array::LargeBinaryArray::from_vec(vec![&b"hello"[..], b"world"]);
|
||||
let section_a = RecordBatch::try_from_iter([("a", Arc::new(blocks) as ArrayRef)]).unwrap();
|
||||
let section_b = record_batch!(("b", Int64, [10i64, 20, 30, 40, 50])).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
// Arbitrary, deliberately non-64-aligned preamble so the first section
|
||||
// must be padded rather than landing at offset 0 by luck.
|
||||
buf.extend_from_slice(&[0xABu8; 7]);
|
||||
let mut pos = buf.len();
|
||||
// The first section's stream begins after padding the 7-byte preamble
|
||||
// up to the next 64-byte boundary.
|
||||
assert_eq!(7 + section_padding(7), IPC_SECTION_ALIGNMENT);
|
||||
write_ipc_section(&mut buf, &mut pos, §ion_a).unwrap();
|
||||
write_ipc_section(&mut buf, &mut pos, §ion_b).unwrap();
|
||||
|
||||
let data = aligned_bytes(&buf);
|
||||
assert_eq!(
|
||||
section_padding(data.as_ptr() as usize),
|
||||
0,
|
||||
"base not aligned"
|
||||
);
|
||||
|
||||
let mut offset = 7;
|
||||
let read_a = read_ipc_section_at(&data, &mut offset).unwrap();
|
||||
let read_b = read_ipc_section_at(&data, &mut offset).unwrap();
|
||||
assert_eq!(read_a, section_a);
|
||||
assert_eq!(read_b, section_b);
|
||||
|
||||
let data_base = data.as_ptr() as usize;
|
||||
let data_end = data_base + data.len();
|
||||
for batch in [&read_a, &read_b] {
|
||||
for buffer in batch.column(0).to_data().buffers() {
|
||||
let ptr = buffer.as_ptr() as usize;
|
||||
assert!(
|
||||
ptr >= data_base && ptr < data_end,
|
||||
"section buffer at {ptr:#x} was realigned out of the input \
|
||||
[{data_base:#x}..{data_end:#x}) — misaligned section",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aligned_multi_batch_section_roundtrip_zero_copy() {
|
||||
// A multi-batch section (e.g. IVF SQ storage chunks) must round-trip
|
||||
// every batch and decode the first batch's buffers zero-copy.
|
||||
let b1 = record_batch!(("v", Int64, [1i64, 2, 3])).unwrap();
|
||||
let b2 = record_batch!(("v", Int64, [4i64, 5])).unwrap();
|
||||
let b3 = record_batch!(("v", Int64, [6i64])).unwrap();
|
||||
|
||||
let mut buf = vec![0xCDu8; 5];
|
||||
let mut pos = buf.len();
|
||||
write_ipc_section_batches(&mut buf, &mut pos, [b1.clone(), b2.clone(), b3.clone()])
|
||||
.unwrap();
|
||||
|
||||
let data = aligned_bytes(&buf);
|
||||
let mut offset = 5;
|
||||
let read = read_ipc_section_batches_at(&data, &mut offset).unwrap();
|
||||
assert_eq!(read, vec![b1, b2, b3]);
|
||||
assert_eq!(offset, buf.len(), "offset should land at section end");
|
||||
|
||||
let data_base = data.as_ptr() as usize;
|
||||
let data_end = data_base + data.len();
|
||||
for buffer in read[0].column(0).to_data().buffers() {
|
||||
let ptr = buffer.as_ptr() as usize;
|
||||
assert!(
|
||||
ptr >= data_base && ptr < data_end,
|
||||
"first batch buffer at {ptr:#x} was realigned out of the input",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1445
File diff suppressed because it is too large
Load Diff
Vendored
+3048
File diff suppressed because it is too large
Load Diff
+176
@@ -0,0 +1,176 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{Array, BooleanArray, GenericListArray, OffsetSizeTrait};
|
||||
use arrow_buffer::{BooleanBufferBuilder, OffsetBuffer, ScalarBuffer};
|
||||
use arrow_schema::Field;
|
||||
|
||||
pub trait ListArrayExt {
|
||||
/// Filters out masked null items from the list array
|
||||
///
|
||||
/// It is legal for a list array to have a null entry with a non-zero length. The
|
||||
/// values inside the entry are "garbage" and should be ignored. This function
|
||||
/// filters the values array to remove the garbage values.
|
||||
///
|
||||
/// The output list will always have zero-length nulls.
|
||||
fn filter_garbage_nulls(&self) -> Self;
|
||||
/// Returns a copy of the list's values array that has been sliced to size
|
||||
///
|
||||
/// It is legal for a list array's offsets to not start with zero. It's also legal
|
||||
/// for a list array's offsets to not extend to the entire values array. This function
|
||||
/// behaves similarly to `values()` except it slices the array so that it starts at
|
||||
/// the first list offset and ends at the last list offset.
|
||||
fn trimmed_values(&self) -> Arc<dyn Array>;
|
||||
/// The offset type of the underlying list array.
|
||||
type Offset: OffsetSizeTrait;
|
||||
/// Returns offsets shifted so the first offset is zero, matching
|
||||
/// [`Self::trimmed_values`].
|
||||
///
|
||||
/// Sliced list arrays (e.g. a filtered batch) keep offsets that reference the
|
||||
/// original values buffer, so combining them with trimmed values produces
|
||||
/// offsets that exceed the values length. Use this together with
|
||||
/// `trimmed_values` when constructing a new list array.
|
||||
fn trimmed_offsets(&self) -> OffsetBuffer<Self::Offset>;
|
||||
}
|
||||
|
||||
impl<OffsetSize: OffsetSizeTrait> ListArrayExt for GenericListArray<OffsetSize> {
|
||||
fn filter_garbage_nulls(&self) -> Self {
|
||||
if self.is_empty() {
|
||||
return self.clone();
|
||||
}
|
||||
let Some(validity) = self.nulls().cloned() else {
|
||||
return self.clone();
|
||||
};
|
||||
|
||||
let mut should_keep = BooleanBufferBuilder::new(self.values().len());
|
||||
|
||||
// Handle case where offsets do not start at 0
|
||||
let preamble_len = self.offsets().first().unwrap().to_usize().unwrap();
|
||||
should_keep.append_n(preamble_len, false);
|
||||
|
||||
let mut new_offsets: Vec<OffsetSize> = Vec::with_capacity(self.len() + 1);
|
||||
new_offsets.push(OffsetSize::zero());
|
||||
let mut cur_len = OffsetSize::zero();
|
||||
for (offset, is_valid) in self.offsets().windows(2).zip(validity.iter()) {
|
||||
let len = offset[1] - offset[0];
|
||||
if is_valid {
|
||||
cur_len += len;
|
||||
should_keep.append_n(len.to_usize().unwrap(), true);
|
||||
new_offsets.push(cur_len);
|
||||
} else {
|
||||
should_keep.append_n(len.to_usize().unwrap(), false);
|
||||
new_offsets.push(cur_len);
|
||||
}
|
||||
}
|
||||
|
||||
// Offsets may not reference entire values buffer
|
||||
let trailer = self.values().len() - should_keep.len();
|
||||
should_keep.append_n(trailer, false);
|
||||
|
||||
let should_keep = should_keep.finish();
|
||||
let should_keep = BooleanArray::new(should_keep, None);
|
||||
let new_values = arrow_select::filter::filter(self.values(), &should_keep).unwrap();
|
||||
let new_offsets = ScalarBuffer::from(new_offsets);
|
||||
let new_offsets = OffsetBuffer::new(new_offsets);
|
||||
|
||||
Self::new(
|
||||
Arc::new(Field::new(
|
||||
"item",
|
||||
self.value_type(),
|
||||
self.values().is_nullable(),
|
||||
)),
|
||||
new_offsets,
|
||||
new_values,
|
||||
Some(validity),
|
||||
)
|
||||
}
|
||||
|
||||
fn trimmed_values(&self) -> Arc<dyn Array> {
|
||||
let first_value = self
|
||||
.offsets()
|
||||
.first()
|
||||
.map(|v| v.to_usize().unwrap())
|
||||
.unwrap_or(0);
|
||||
let last_value = self
|
||||
.offsets()
|
||||
.last()
|
||||
.map(|v| v.to_usize().unwrap())
|
||||
.unwrap_or(0);
|
||||
self.values().slice(first_value, last_value - first_value)
|
||||
}
|
||||
|
||||
type Offset = OffsetSize;
|
||||
|
||||
fn trimmed_offsets(&self) -> OffsetBuffer<OffsetSize> {
|
||||
let offsets = self.offsets();
|
||||
let Some(&first) = offsets.first() else {
|
||||
return offsets.clone();
|
||||
};
|
||||
if first == OffsetSize::zero() {
|
||||
return offsets.clone();
|
||||
}
|
||||
let shifted: Vec<OffsetSize> = offsets.iter().map(|&o| o - first).collect();
|
||||
OffsetBuffer::new(ScalarBuffer::from(shifted))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{ListArray, UInt64Array};
|
||||
use arrow_buffer::{BooleanBuffer, NullBuffer, OffsetBuffer, ScalarBuffer};
|
||||
use arrow_schema::{DataType, Field};
|
||||
|
||||
use super::ListArrayExt;
|
||||
|
||||
#[test]
|
||||
fn test_filter_garbage_nulls() {
|
||||
let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
let offsets = ScalarBuffer::<i32>::from(vec![2, 5, 8, 9]);
|
||||
let offsets = OffsetBuffer::new(offsets);
|
||||
let list_validity = NullBuffer::new(BooleanBuffer::from(vec![true, false, true]));
|
||||
let list_arr = ListArray::new(
|
||||
Arc::new(Field::new("item", DataType::UInt64, true)),
|
||||
offsets,
|
||||
Arc::new(items),
|
||||
Some(list_validity.clone()),
|
||||
);
|
||||
|
||||
let filtered = list_arr.filter_garbage_nulls();
|
||||
|
||||
let expected_items = UInt64Array::from(vec![2, 3, 4, 8]);
|
||||
let offsets = ScalarBuffer::<i32>::from(vec![0, 3, 3, 4]);
|
||||
let expected = ListArray::new(
|
||||
Arc::new(Field::new("item", DataType::UInt64, false)),
|
||||
OffsetBuffer::new(offsets),
|
||||
Arc::new(expected_items),
|
||||
Some(list_validity),
|
||||
);
|
||||
|
||||
assert_eq!(filtered, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_values() {
|
||||
let items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
let offsets = ScalarBuffer::<i32>::from(vec![2, 5, 6, 8, 9]);
|
||||
let offsets = OffsetBuffer::new(offsets);
|
||||
let list_arr = ListArray::new(
|
||||
Arc::new(Field::new("item", DataType::UInt64, true)),
|
||||
offsets,
|
||||
Arc::new(items),
|
||||
None,
|
||||
);
|
||||
let list_arr = list_arr.slice(1, 2);
|
||||
|
||||
let trimmed = list_arr.trimmed_values();
|
||||
|
||||
let expected_items = UInt64Array::from(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
let expected_items = expected_items.slice(5, 3);
|
||||
|
||||
assert_eq!(trimmed.as_ref(), &expected_items);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use arrow_array::{Array, RecordBatch};
|
||||
use arrow_data::ArrayData;
|
||||
|
||||
/// Counts memory used by buffers of Arrow arrays and RecordBatches.
|
||||
///
|
||||
/// This is meant to capture how much memory is being used by the Arrow data
|
||||
/// structures as they are. It does not represent the memory used if the data
|
||||
/// were to be serialized and then deserialized. In particular:
|
||||
///
|
||||
/// * This does not double count memory used by buffers shared by multiple
|
||||
/// arrays or batches. Round-tripped data may use more memory because of this.
|
||||
/// * This counts the **total** size of the buffers, even if the array is a slice.
|
||||
/// Round-tripped data may use less memory because of this.
|
||||
#[derive(Default)]
|
||||
pub struct MemoryAccumulator {
|
||||
seen: HashSet<usize>,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl MemoryAccumulator {
|
||||
pub fn record_array(&mut self, array: &dyn Array) {
|
||||
let data = array.to_data();
|
||||
self.record_array_data(&data);
|
||||
}
|
||||
|
||||
fn record_array_data(&mut self, data: &ArrayData) {
|
||||
for buffer in data.buffers() {
|
||||
let ptr = buffer.as_ptr();
|
||||
if self.seen.insert(ptr as usize) {
|
||||
self.total += buffer.capacity();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nulls) = data.nulls() {
|
||||
let null_buf = nulls.inner().inner();
|
||||
let ptr = null_buf.as_ptr();
|
||||
if self.seen.insert(ptr as usize) {
|
||||
self.total += null_buf.capacity();
|
||||
}
|
||||
}
|
||||
|
||||
for child in data.child_data() {
|
||||
self.record_array_data(child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_batch(&mut self, batch: &RecordBatch) {
|
||||
for array in batch.columns() {
|
||||
self.record_array(array);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total(&self) -> usize {
|
||||
self.total
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::Int32Array;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_memory_accumulator() {
|
||||
let batch = RecordBatch::try_new(
|
||||
Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
|
||||
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
|
||||
)
|
||||
.unwrap();
|
||||
let slice = batch.slice(1, 2);
|
||||
|
||||
let mut acc = MemoryAccumulator::default();
|
||||
|
||||
// Should record whole buffer, not just slice
|
||||
acc.record_batch(&slice);
|
||||
assert_eq!(acc.total(), 3 * std::mem::size_of::<i32>());
|
||||
|
||||
// Should not double count
|
||||
acc.record_batch(&slice);
|
||||
assert_eq!(acc.total(), 3 * std::mem::size_of::<i32>());
|
||||
}
|
||||
}
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
use arrow_array::{ArrayRef, UInt64Array, make_array};
|
||||
use arrow_buffer::Buffer;
|
||||
use arrow_data::ArrayDataBuilder;
|
||||
use arrow_schema::{ArrowError, DataType};
|
||||
use arrow_select::take::take;
|
||||
|
||||
use crate::DataTypeExt;
|
||||
|
||||
type Result<T> = std::result::Result<T, ArrowError>;
|
||||
|
||||
pub const INLINE_VALUE_MAX_BYTES: usize = 32;
|
||||
|
||||
pub fn extract_scalar_value(array: &ArrayRef, idx: usize) -> Result<ArrayRef> {
|
||||
if idx >= array.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Scalar index out of bounds".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
take(array.as_ref(), &UInt64Array::from(vec![idx as u64]), None)
|
||||
}
|
||||
|
||||
fn read_u32(buf: &[u8], offset: &mut usize) -> Result<u32> {
|
||||
if *offset + 4 > buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar value buffer: unexpected EOF".to_string(),
|
||||
));
|
||||
}
|
||||
let bytes = [
|
||||
buf[*offset],
|
||||
buf[*offset + 1],
|
||||
buf[*offset + 2],
|
||||
buf[*offset + 3],
|
||||
];
|
||||
*offset += 4;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_bytes<'a>(buf: &'a [u8], offset: &mut usize, len: usize) -> Result<&'a [u8]> {
|
||||
if *offset + len > buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar value buffer: unexpected EOF".to_string(),
|
||||
));
|
||||
}
|
||||
let slice = &buf[*offset..*offset + len];
|
||||
*offset += len;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
fn write_u32(out: &mut Vec<u8>, v: u32) {
|
||||
out.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
|
||||
fn write_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
pub fn encode_scalar_value_buffer(scalar: &ArrayRef) -> Result<Vec<u8>> {
|
||||
if scalar.len() != 1 || scalar.null_count() != 0 {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Scalar value buffer must be a single non-null value".to_string(),
|
||||
));
|
||||
}
|
||||
let data = scalar.to_data();
|
||||
if data.offset() != 0 {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Scalar value buffer must have offset=0".to_string(),
|
||||
));
|
||||
}
|
||||
if !data.child_data().is_empty() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Scalar value buffer does not support nested types".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Minimal format (RFC): store the Arrow value buffers for a length-1 array.
|
||||
// Null bitmap and child data are intentionally not supported here.
|
||||
//
|
||||
// | u32 num_buffers |
|
||||
// | u32 buffer_0_len | ... | u32 buffer_{n-1}_len |
|
||||
// | buffer_0 bytes | ... | buffer_{n-1} bytes |
|
||||
let mut out = Vec::with_capacity(128);
|
||||
let buffers = data.buffers();
|
||||
write_u32(&mut out, buffers.len() as u32);
|
||||
for b in buffers {
|
||||
write_u32(&mut out, b.len() as u32);
|
||||
}
|
||||
for b in buffers {
|
||||
write_bytes(&mut out, b.as_slice());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn decode_scalar_from_value_buffer(
|
||||
data_type: &DataType,
|
||||
value_buffer: &[u8],
|
||||
) -> Result<ArrayRef> {
|
||||
if matches!(
|
||||
data_type,
|
||||
DataType::Struct(_) | DataType::FixedSizeList(_, _)
|
||||
) {
|
||||
return Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Scalar value buffer does not support nested data type {:?}",
|
||||
data_type
|
||||
)));
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
let num_buffers = read_u32(value_buffer, &mut offset)? as usize;
|
||||
let buffer_lens = (0..num_buffers)
|
||||
.map(|_| read_u32(value_buffer, &mut offset).map(|l| l as usize))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mut buffers = Vec::with_capacity(num_buffers);
|
||||
for len in buffer_lens {
|
||||
let bytes = read_bytes(value_buffer, &mut offset, len)?;
|
||||
buffers.push(Buffer::from_vec(bytes.to_vec()));
|
||||
}
|
||||
|
||||
if offset != value_buffer.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar value buffer: trailing bytes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut builder = ArrayDataBuilder::new(data_type.clone())
|
||||
.len(1)
|
||||
.null_count(0);
|
||||
for b in buffers {
|
||||
builder = builder.add_buffer(b);
|
||||
}
|
||||
Ok(make_array(builder.build()?))
|
||||
}
|
||||
|
||||
pub fn decode_scalar_from_inline_value(
|
||||
data_type: &DataType,
|
||||
inline_value: &[u8],
|
||||
) -> Result<ArrayRef> {
|
||||
// I expect our input to be safe here, but I added some debug_assert_eq statements just in case.
|
||||
// If they are triggered, we may need to change them to return actual errors.
|
||||
//
|
||||
// Boolean values are bit-packed in Arrow and therefore are not "fixed-stride" in bytes.
|
||||
// As a result, `byte_width_opt()` returns `None` for `DataType::Boolean`, even though a
|
||||
// length-1 scalar can be represented inline using a single byte (matching `try_inline_value`).
|
||||
if matches!(data_type, DataType::Boolean) {
|
||||
debug_assert_eq!(
|
||||
inline_value.len(),
|
||||
1,
|
||||
"Invalid boolean inline scalar length (expected 1 byte, got {})",
|
||||
inline_value.len()
|
||||
);
|
||||
} else if let Some(byte_width) = data_type.byte_width_opt() {
|
||||
debug_assert_eq!(
|
||||
inline_value.len(),
|
||||
byte_width,
|
||||
"Inline constant length mismatch for {:?}: expected {} bytes but got {}",
|
||||
data_type,
|
||||
byte_width,
|
||||
inline_value.len()
|
||||
);
|
||||
}
|
||||
|
||||
let data = ArrayDataBuilder::new(data_type.clone())
|
||||
.len(1)
|
||||
.null_count(0)
|
||||
.add_buffer(Buffer::from_vec(inline_value.to_vec()))
|
||||
.build()?;
|
||||
Ok(make_array(data))
|
||||
}
|
||||
|
||||
pub fn try_inline_value(scalar: &ArrayRef) -> Option<Vec<u8>> {
|
||||
if scalar.null_count() != 0 || scalar.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let data = scalar.to_data();
|
||||
if !data.child_data().is_empty() {
|
||||
return None;
|
||||
}
|
||||
if data.buffers().len() != 1 {
|
||||
return None;
|
||||
}
|
||||
let bytes = data.buffers()[0].as_slice();
|
||||
if bytes.len() > INLINE_VALUE_MAX_BYTES {
|
||||
return None;
|
||||
}
|
||||
Some(bytes.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{
|
||||
BooleanArray, DictionaryArray, FixedSizeBinaryArray, Int8Array, Int32Array, StringArray,
|
||||
cast::AsArray, types::Int8Type,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_extract_scalar_value() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), None, Some(3)]));
|
||||
let scalar = extract_scalar_value(&array, 2).unwrap();
|
||||
assert_eq!(scalar.len(), 1);
|
||||
assert_eq!(
|
||||
scalar
|
||||
.as_primitive::<arrow_array::types::Int32Type>()
|
||||
.value(0),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_scalar_value_from_full_dictionary() {
|
||||
let values = Arc::new(StringArray::from(
|
||||
(0..=i8::MAX)
|
||||
.map(|value| format!("value-{value}"))
|
||||
.collect::<Vec<_>>(),
|
||||
));
|
||||
let keys = Int8Array::from((0..=i8::MAX).collect::<Vec<_>>());
|
||||
let array: ArrayRef = Arc::new(DictionaryArray::<Int8Type>::new(keys, values));
|
||||
|
||||
let scalar = extract_scalar_value(&array, i8::MAX as usize).unwrap();
|
||||
|
||||
let scalar = scalar.as_dictionary::<Int8Type>();
|
||||
assert_eq!(scalar.len(), 1);
|
||||
assert_eq!(scalar.key(0), Some(i8::MAX as usize));
|
||||
assert_eq!(scalar.values().len(), i8::MAX as usize + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scalar_value_buffer_utf8_round_trip() {
|
||||
let scalar: ArrayRef = Arc::new(StringArray::from(vec!["hello"]));
|
||||
let buf = encode_scalar_value_buffer(&scalar).unwrap();
|
||||
let decoded = decode_scalar_from_value_buffer(&DataType::Utf8, &buf).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded.null_count(), 0);
|
||||
assert_eq!(decoded.as_string::<i32>().value(0), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scalar_value_buffer_fixed_size_binary_round_trip() {
|
||||
let val = vec![0xABu8; 33];
|
||||
let scalar: ArrayRef = Arc::new(
|
||||
FixedSizeBinaryArray::try_from_sparse_iter_with_size(
|
||||
std::iter::once(Some(val.as_slice())),
|
||||
33,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let buf = encode_scalar_value_buffer(&scalar).unwrap();
|
||||
let decoded =
|
||||
decode_scalar_from_value_buffer(&DataType::FixedSizeBinary(33), &buf).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded.as_fixed_size_binary().value(0), val.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inline_value_boolean_round_trip() {
|
||||
let scalar: ArrayRef = Arc::new(BooleanArray::from_iter([Some(true)]));
|
||||
let inline = try_inline_value(&scalar).unwrap();
|
||||
let decoded = decode_scalar_from_inline_value(&DataType::Boolean, &inline).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded.null_count(), 0);
|
||||
assert!(decoded.as_boolean().value(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scalar_value_buffer_rejects_nested_type() {
|
||||
let field = Arc::new(arrow_schema::Field::new("item", DataType::Int32, false));
|
||||
let list: ArrayRef = Arc::new(arrow_array::FixedSizeListArray::new(
|
||||
field,
|
||||
2,
|
||||
Arc::new(Int32Array::from(vec![1, 2])),
|
||||
None,
|
||||
));
|
||||
let scalar = list.slice(0, 1);
|
||||
assert!(encode_scalar_value_buffer(&scalar).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_scalar_from_value_buffer_rejects_nested_type() {
|
||||
let buf = Vec::<u8>::new();
|
||||
let res =
|
||||
decode_scalar_from_value_buffer(&DataType::Struct(arrow_schema::Fields::empty()), &buf);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_scalar_from_value_buffer_trailing_bytes() {
|
||||
// num_buffers = 0, plus an extra byte
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&0u32.to_le_bytes());
|
||||
bytes.push(1);
|
||||
let res = decode_scalar_from_value_buffer(&DataType::Int32, &bytes);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Extension to arrow schema
|
||||
|
||||
use arrow_schema::{ArrowError, DataType, Field, FieldRef, Schema};
|
||||
|
||||
use crate::{ARROW_EXT_NAME_KEY, BLOB_META_KEY, BLOB_V2_EXT_NAME};
|
||||
|
||||
pub enum Indentation {
|
||||
OneLine,
|
||||
MultiLine(u8),
|
||||
}
|
||||
|
||||
impl Indentation {
|
||||
fn value(&self) -> String {
|
||||
match self {
|
||||
Self::OneLine => "".to_string(),
|
||||
Self::MultiLine(spaces) => " ".repeat(*spaces as usize),
|
||||
}
|
||||
}
|
||||
|
||||
fn deepen(&self) -> Self {
|
||||
match self {
|
||||
Self::OneLine => Self::OneLine,
|
||||
Self::MultiLine(spaces) => Self::MultiLine(spaces + 2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the functionality of [arrow_schema::Field].
|
||||
pub trait FieldExt {
|
||||
/// Create a compact string representation of the field
|
||||
///
|
||||
/// This is intended for display purposes and not for serialization
|
||||
fn to_compact_string(&self, indent: Indentation) -> String;
|
||||
|
||||
/// Check if the field is marked as a packed struct
|
||||
fn is_packed_struct(&self) -> bool;
|
||||
|
||||
/// Check if the field is marked as a blob
|
||||
fn is_blob(&self) -> bool;
|
||||
|
||||
/// Check if the field is marked as a blob
|
||||
fn is_blob_v2(&self) -> bool;
|
||||
}
|
||||
|
||||
impl FieldExt for Field {
|
||||
fn to_compact_string(&self, indent: Indentation) -> String {
|
||||
let mut result = format!("{}: ", self.name().clone());
|
||||
match self.data_type() {
|
||||
DataType::Struct(fields) => {
|
||||
result += "{";
|
||||
result += &indent.value();
|
||||
for (field_idx, field) in fields.iter().enumerate() {
|
||||
result += field.to_compact_string(indent.deepen()).as_str();
|
||||
if field_idx < fields.len() - 1 {
|
||||
result += ",";
|
||||
}
|
||||
result += indent.value().as_str();
|
||||
}
|
||||
result += "}";
|
||||
}
|
||||
DataType::List(field)
|
||||
| DataType::LargeList(field)
|
||||
| DataType::ListView(field)
|
||||
| DataType::LargeListView(field) => {
|
||||
result += "[";
|
||||
result += field.to_compact_string(indent.deepen()).as_str();
|
||||
result += "]";
|
||||
}
|
||||
DataType::FixedSizeList(child, dimension) => {
|
||||
result += &format!(
|
||||
"[{}; {}]",
|
||||
child.to_compact_string(indent.deepen()),
|
||||
dimension
|
||||
);
|
||||
}
|
||||
DataType::Dictionary(key_type, value_type) => {
|
||||
result += &value_type.to_string();
|
||||
result += "@";
|
||||
result += &key_type.to_string();
|
||||
}
|
||||
_ => {
|
||||
result += &self.data_type().to_string();
|
||||
}
|
||||
}
|
||||
if self.is_nullable() {
|
||||
result += "?";
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Check if field has metadata `packed` set to true, this check is case insensitive.
|
||||
fn is_packed_struct(&self) -> bool {
|
||||
let field_metadata = self.metadata();
|
||||
const PACKED_KEYS: [&str; 2] = ["packed", "lance-encoding:packed"];
|
||||
PACKED_KEYS.iter().any(|key| {
|
||||
field_metadata
|
||||
.get(*key)
|
||||
.map(|value| value.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_blob(&self) -> bool {
|
||||
let field_metadata = self.metadata();
|
||||
field_metadata.get(BLOB_META_KEY).is_some()
|
||||
|| field_metadata
|
||||
.get(ARROW_EXT_NAME_KEY)
|
||||
.map(|value| value == BLOB_V2_EXT_NAME)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_blob_v2(&self) -> bool {
|
||||
let field_metadata = self.metadata();
|
||||
field_metadata
|
||||
.get(ARROW_EXT_NAME_KEY)
|
||||
.map(|value| value == BLOB_V2_EXT_NAME)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the functionality of [arrow_schema::Schema].
|
||||
pub trait SchemaExt {
|
||||
/// Create a new [`Schema`] with one extra field.
|
||||
fn try_with_column(&self, field: Field) -> std::result::Result<Schema, ArrowError>;
|
||||
|
||||
fn try_with_column_at(
|
||||
&self,
|
||||
index: usize,
|
||||
field: Field,
|
||||
) -> std::result::Result<Schema, ArrowError>;
|
||||
|
||||
fn field_names(&self) -> Vec<&String>;
|
||||
|
||||
fn without_column(&self, column_name: &str) -> Schema;
|
||||
|
||||
/// Create a compact string representation of the schema
|
||||
///
|
||||
/// This is intended for display purposes and not for serialization
|
||||
fn to_compact_string(&self, indent: Indentation) -> String;
|
||||
}
|
||||
|
||||
impl SchemaExt for Schema {
|
||||
fn try_with_column(&self, field: Field) -> std::result::Result<Schema, ArrowError> {
|
||||
if self.column_with_name(field.name()).is_some() {
|
||||
return Err(ArrowError::SchemaError(format!(
|
||||
"Can not append column {} on schema: {:?}",
|
||||
field.name(),
|
||||
self
|
||||
)));
|
||||
};
|
||||
let mut fields: Vec<FieldRef> = self.fields().iter().cloned().collect();
|
||||
fields.push(FieldRef::new(field));
|
||||
Ok(Self::new_with_metadata(fields, self.metadata.clone()))
|
||||
}
|
||||
|
||||
fn try_with_column_at(
|
||||
&self,
|
||||
index: usize,
|
||||
field: Field,
|
||||
) -> std::result::Result<Schema, ArrowError> {
|
||||
if self.column_with_name(field.name()).is_some() {
|
||||
return Err(ArrowError::SchemaError(format!(
|
||||
"Failed to modify schema: Inserting column {} would create a duplicate column in schema: {:?}",
|
||||
field.name(),
|
||||
self
|
||||
)));
|
||||
};
|
||||
let mut fields: Vec<FieldRef> = self.fields().iter().cloned().collect();
|
||||
fields.insert(index, FieldRef::new(field));
|
||||
Ok(Self::new_with_metadata(fields, self.metadata.clone()))
|
||||
}
|
||||
|
||||
/// Project the schema to remove the given column.
|
||||
///
|
||||
/// This only works on top-level fields right now. If a field does not exist,
|
||||
/// the schema will be returned as is.
|
||||
fn without_column(&self, column_name: &str) -> Schema {
|
||||
let fields: Vec<FieldRef> = self
|
||||
.fields()
|
||||
.iter()
|
||||
.filter(|f| f.name() != column_name)
|
||||
.cloned()
|
||||
.collect();
|
||||
Self::new_with_metadata(fields, self.metadata.clone())
|
||||
}
|
||||
|
||||
fn field_names(&self) -> Vec<&String> {
|
||||
self.fields().iter().map(|f| f.name()).collect()
|
||||
}
|
||||
|
||||
fn to_compact_string(&self, indent: Indentation) -> String {
|
||||
let mut result = "{".to_string();
|
||||
result += &indent.value();
|
||||
for (field_idx, field) in self.fields.iter().enumerate() {
|
||||
result += field.to_compact_string(indent.deepen()).as_str();
|
||||
if field_idx < self.fields.len() - 1 {
|
||||
result += ",";
|
||||
}
|
||||
result += indent.value().as_str();
|
||||
}
|
||||
result += "}";
|
||||
result
|
||||
}
|
||||
}
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Utilities for working with streams of [`RecordBatch`].
|
||||
|
||||
use arrow_array::RecordBatch;
|
||||
use arrow_schema::{ArrowError, SchemaRef};
|
||||
use futures::stream::{self, Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::deepcopy::deep_copy_batch_sliced;
|
||||
|
||||
/// Rechunks a stream of [`RecordBatch`] so that each output batch has
|
||||
/// approximately `target_bytes` of array data.
|
||||
///
|
||||
/// Small input batches are accumulated (by concatenation) until at least
|
||||
/// `min_bytes` of data has been collected. If the resulting batch exceeds
|
||||
/// `max_bytes`, it is sliced into roughly equal pieces of ~`max_bytes`
|
||||
/// (assuming uniform row sizes).
|
||||
pub fn rechunk_stream_by_size<S, E>(
|
||||
input: S,
|
||||
input_schema: SchemaRef,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
) -> impl Stream<Item = Result<RecordBatch, E>>
|
||||
where
|
||||
S: Stream<Item = Result<RecordBatch, E>>,
|
||||
E: From<ArrowError>,
|
||||
{
|
||||
rechunk_stream_by_size_inner(input, input_schema, min_bytes, max_bytes, false)
|
||||
}
|
||||
|
||||
/// Like [`rechunk_stream_by_size`] but deep-copies slices so that
|
||||
/// `get_array_memory_size` reflects the true size of each output batch.
|
||||
///
|
||||
/// After a normal `RecordBatch::slice`, the backing buffers are shared with
|
||||
/// the original batch, so `get_array_memory_size` still reports the full
|
||||
/// parent size. This variant deep-copies every slice produced during the
|
||||
/// splitting phase, which allows the stream to detect and re-split slices
|
||||
/// that still exceed `max_bytes` (e.g. because a single row is much larger
|
||||
/// than average).
|
||||
///
|
||||
/// The deep copy is a last resort and potentially expensive for large
|
||||
/// batches. However, it is only performed when a batch actually needs to be
|
||||
/// sliced — batches that are already within the target range pass through at
|
||||
/// zero cost. Use this only when the hard cap on `max_bytes` is a
|
||||
/// correctness requirement, not merely a performance hint.
|
||||
pub fn rechunk_stream_by_size_deep_copy<S, E>(
|
||||
input: S,
|
||||
input_schema: SchemaRef,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
) -> impl Stream<Item = Result<RecordBatch, E>>
|
||||
where
|
||||
S: Stream<Item = Result<RecordBatch, E>>,
|
||||
E: From<ArrowError>,
|
||||
{
|
||||
rechunk_stream_by_size_inner(input, input_schema, min_bytes, max_bytes, true)
|
||||
}
|
||||
|
||||
fn rechunk_stream_by_size_inner<S, E>(
|
||||
input: S,
|
||||
input_schema: SchemaRef,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
deep_copy: bool,
|
||||
) -> impl Stream<Item = Result<RecordBatch, E>>
|
||||
where
|
||||
S: Stream<Item = Result<RecordBatch, E>>,
|
||||
E: From<ArrowError>,
|
||||
{
|
||||
stream::try_unfold(
|
||||
RechunkState {
|
||||
input: Box::pin(input),
|
||||
accumulated: Vec::new(),
|
||||
acc_bytes: 0,
|
||||
done: false,
|
||||
input_schema,
|
||||
min_bytes,
|
||||
max_bytes,
|
||||
deep_copy,
|
||||
},
|
||||
|mut state| async move {
|
||||
if state.done && state.accumulated.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Pull batches until we reach the byte target or exhaust input.
|
||||
// Always pull at least one batch so that min_bytes=0 works.
|
||||
while !state.done && (state.accumulated.is_empty() || state.acc_bytes < state.min_bytes)
|
||||
{
|
||||
match state.input.next().await {
|
||||
Some(Ok(batch)) => {
|
||||
state.acc_bytes += batch.get_array_memory_size();
|
||||
state.accumulated.push(batch);
|
||||
}
|
||||
Some(Err(e)) => return Err(e),
|
||||
None => {
|
||||
state.done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.accumulated.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Fast path: if the first accumulated batch already meets the
|
||||
// byte threshold, deliver it directly instead of concatenating
|
||||
// everything together (which would just get sliced back apart).
|
||||
if state.accumulated.len() > 1
|
||||
&& state.accumulated[0].get_array_memory_size() >= state.min_bytes
|
||||
{
|
||||
let b = state.accumulated.remove(0);
|
||||
state.acc_bytes -= b.get_array_memory_size();
|
||||
return Ok(Some((b, state)));
|
||||
}
|
||||
|
||||
let batch = if state.accumulated.len() == 1 {
|
||||
state.accumulated.pop().unwrap()
|
||||
} else {
|
||||
let b =
|
||||
arrow_select::concat::concat_batches(&state.input_schema, &state.accumulated)
|
||||
.map_err(E::from)?;
|
||||
state.accumulated.clear();
|
||||
b
|
||||
};
|
||||
state.acc_bytes = 0;
|
||||
|
||||
// Slice the batch into ~max_bytes pieces assuming uniform row sizes.
|
||||
let mut slices =
|
||||
slice_batch(batch, state.max_bytes, state.deep_copy).map_err(E::from)?;
|
||||
|
||||
if slices.len() == 1 {
|
||||
Ok(Some((slices.pop().unwrap(), state)))
|
||||
} else {
|
||||
let first = slices.remove(0);
|
||||
|
||||
// Stash leftover slices for subsequent iterations.
|
||||
for a in &slices {
|
||||
state.acc_bytes += a.get_array_memory_size();
|
||||
}
|
||||
state.accumulated = slices;
|
||||
|
||||
Ok(Some((first, state)))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Slice a batch into pieces of at most `max_bytes`.
|
||||
///
|
||||
/// When `deep_copy` is false, slices share buffers with the original batch
|
||||
/// and `get_array_memory_size` will still report the parent buffer size.
|
||||
/// This is fine when the caller only needs approximate sizing.
|
||||
///
|
||||
/// When `deep_copy` is true, each slice is deep-copied so that
|
||||
/// `get_array_memory_size` reflects the true size. If a deep-copied slice
|
||||
/// still exceeds `max_bytes` (due to non-uniform row sizes), it is
|
||||
/// recursively split until every piece is within budget or contains only a
|
||||
/// single row.
|
||||
fn slice_batch(
|
||||
batch: RecordBatch,
|
||||
max_bytes: usize,
|
||||
deep_copy: bool,
|
||||
) -> Result<Vec<RecordBatch>, ArrowError> {
|
||||
let batch_bytes = batch.get_array_memory_size();
|
||||
let num_rows = batch.num_rows();
|
||||
|
||||
if batch_bytes <= max_bytes {
|
||||
return Ok(vec![batch]);
|
||||
}
|
||||
|
||||
if num_rows <= 1 {
|
||||
// A single row cannot be split further, but the size just measured may
|
||||
// belong to its source buffer rather than to the row:
|
||||
// `get_array_memory_size` reports whole buffers, and a slice shares
|
||||
// them. Returning it uncopied hands the caller that inflated figure --
|
||||
// a 100 KB row inside a 200 MB buffer measures as 200 MB, and a caller
|
||||
// budgeting on it concludes the row cannot fit anywhere.
|
||||
if deep_copy {
|
||||
return Ok(vec![deep_copy_batch_sliced(&batch)?]);
|
||||
}
|
||||
return Ok(vec![batch]);
|
||||
}
|
||||
|
||||
let rows_per_chunk = (max_bytes as u64 * num_rows as u64 / batch_bytes as u64).max(1) as usize;
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut offset = 0;
|
||||
while offset < num_rows {
|
||||
let len = rows_per_chunk.min(num_rows - offset);
|
||||
let slice = batch.slice(offset, len);
|
||||
if deep_copy {
|
||||
let copied = deep_copy_batch_sliced(&slice)?;
|
||||
// Recurse: the deep-copied slice has accurate sizes, so if it
|
||||
// still exceeds max_bytes we can split further.
|
||||
result.extend(slice_batch(copied, max_bytes, true)?);
|
||||
} else {
|
||||
result.push(slice);
|
||||
}
|
||||
offset += len;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Internal state for [`rechunk_stream`].
|
||||
///
|
||||
/// Kept as a named struct so the `try_unfold` closure stays readable.
|
||||
struct RechunkState<S> {
|
||||
input: Pin<Box<S>>,
|
||||
accumulated: Vec<RecordBatch>,
|
||||
acc_bytes: usize,
|
||||
done: bool,
|
||||
input_schema: SchemaRef,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
deep_copy: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::Int32Array;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
use futures::executor::block_on;
|
||||
|
||||
/// A single row that shares a large source buffer must report its own
|
||||
/// size, not the buffer's. Callers budget on this figure, and an inflated
|
||||
/// one makes a small row look impossible to place.
|
||||
#[test]
|
||||
fn a_single_row_slice_reports_its_own_size() {
|
||||
use arrow_array::LargeStringArray;
|
||||
|
||||
let row = "x".repeat(100 * 1024);
|
||||
let values: Vec<&str> = (0..2000).map(|_| row.as_str()).collect();
|
||||
let schema = Arc::new(Schema::new(vec![Field::new(
|
||||
"text",
|
||||
DataType::LargeUtf8,
|
||||
false,
|
||||
)]));
|
||||
let batch =
|
||||
RecordBatch::try_new(schema, vec![Arc::new(LargeStringArray::from(values))]).unwrap();
|
||||
let whole = batch.get_array_memory_size();
|
||||
|
||||
let one_row = batch.slice(7, 1);
|
||||
assert_eq!(
|
||||
one_row.get_array_memory_size(),
|
||||
whole,
|
||||
"a slice shares its source buffer, which is the reason this test exists"
|
||||
);
|
||||
|
||||
// Budget far below the row so the split recurses to the single-row floor.
|
||||
let chunks = slice_batch(one_row, 1024, true).unwrap();
|
||||
assert_eq!(chunks.len(), 1);
|
||||
let measured = chunks[0].get_array_memory_size();
|
||||
assert_eq!(chunks[0].num_rows(), 1);
|
||||
assert!(
|
||||
measured < whole / 100,
|
||||
"single row still measured as {measured} bytes against a {whole} byte source"
|
||||
);
|
||||
}
|
||||
|
||||
fn make_batch(num_rows: usize) -> RecordBatch {
|
||||
let schema = test_schema();
|
||||
let values: Vec<i32> = (0..num_rows as i32).collect();
|
||||
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(values))]).unwrap()
|
||||
}
|
||||
|
||||
fn test_schema() -> SchemaRef {
|
||||
Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]))
|
||||
}
|
||||
|
||||
fn collect_rechunked(
|
||||
batches: Vec<RecordBatch>,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
) -> Vec<RecordBatch> {
|
||||
let input = stream::iter(batches.into_iter().map(Ok::<_, ArrowError>));
|
||||
let rechunked = rechunk_stream_by_size(input, test_schema(), min_bytes, max_bytes);
|
||||
block_on(rechunked.collect::<Vec<_>>())
|
||||
.into_iter()
|
||||
.map(|r| r.unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn total_rows(batches: &[RecordBatch]) -> usize {
|
||||
batches.iter().map(|b| b.num_rows()).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_stream() {
|
||||
let result = collect_rechunked(vec![], 100, 200);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_batch_passthrough() {
|
||||
let batch = make_batch(100);
|
||||
let bytes = batch.get_array_memory_size();
|
||||
// Batch is between min and max — should pass through as-is.
|
||||
let result = collect_rechunked(vec![batch], bytes / 2, bytes * 2);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].num_rows(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_small_batches_concatenated() {
|
||||
let one_batch_bytes = make_batch(10).get_array_memory_size();
|
||||
let batches: Vec<_> = (0..8).map(|_| make_batch(10)).collect();
|
||||
// min = 5 batches worth, max = 10 batches worth.
|
||||
let result = collect_rechunked(batches, one_batch_bytes * 5, one_batch_bytes * 10);
|
||||
assert_eq!(total_rows(&result), 80);
|
||||
// Should have been concatenated into fewer batches than the 8 inputs.
|
||||
assert!(
|
||||
result.len() < 8,
|
||||
"expected fewer output batches, got {}",
|
||||
result.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_batch_sliced() {
|
||||
let batch = make_batch(1000);
|
||||
let bytes = batch.get_array_memory_size();
|
||||
let result = collect_rechunked(vec![batch], bytes / 8, bytes / 4);
|
||||
assert_eq!(total_rows(&result), 1000);
|
||||
assert!(
|
||||
result.len() >= 4,
|
||||
"expected at least 4 slices, got {}",
|
||||
result.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sliced_leftovers_are_not_recombined() {
|
||||
// Key test for the fast-path optimisation. When a large batch is
|
||||
// sliced, leftover slices should be delivered one-at-a-time without
|
||||
// being concatenated back together. We verify this by checking that
|
||||
// every output buffer pointer falls inside the original batch's
|
||||
// allocation (i.e. they are all zero-copy slices, not fresh copies).
|
||||
let batch = make_batch(1000);
|
||||
let bytes = batch.get_array_memory_size();
|
||||
let orig_data = batch.column(0).to_data();
|
||||
let orig_buf = &orig_data.buffers()[0];
|
||||
let orig_start = orig_buf.as_ptr() as usize;
|
||||
let orig_end = orig_start + orig_buf.len();
|
||||
|
||||
let result = collect_rechunked(vec![batch], bytes / 8, bytes / 4);
|
||||
|
||||
assert_eq!(total_rows(&result), 1000);
|
||||
assert!(result.len() >= 4);
|
||||
|
||||
for (i, b) in result.iter().enumerate() {
|
||||
let ptr = b.column(0).to_data().buffers()[0].as_ptr() as usize;
|
||||
assert!(
|
||||
ptr >= orig_start && ptr < orig_end,
|
||||
"slice {i} buffer at {ptr:#x} is outside the original allocation \
|
||||
[{orig_start:#x}, {orig_end:#x}) — it was re-concatenated"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flush_remainder_on_stream_end() {
|
||||
// Data below min_bytes should still be flushed when the stream ends.
|
||||
let batch = make_batch(10);
|
||||
let bytes = batch.get_array_memory_size();
|
||||
let result = collect_rechunked(vec![batch], bytes * 100, bytes * 200);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].num_rows(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_then_small_batches() {
|
||||
// After a large batch is fully drained, subsequent small batches
|
||||
// should be accumulated normally.
|
||||
let large = make_batch(1000);
|
||||
let small_bytes = make_batch(10).get_array_memory_size();
|
||||
let batches = vec![
|
||||
large,
|
||||
make_batch(10),
|
||||
make_batch(10),
|
||||
make_batch(10),
|
||||
make_batch(10),
|
||||
make_batch(10),
|
||||
];
|
||||
let result = collect_rechunked(batches, small_bytes * 3, small_bytes * 100);
|
||||
assert_eq!(total_rows(&result), 1050);
|
||||
// The large batch should appear (possibly sliced) followed by
|
||||
// concatenated small batches, so we should have fewer output batches
|
||||
// than the 6 inputs.
|
||||
assert!(result.len() < 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_row_preservation_across_slicing() {
|
||||
// Verify that every input row appears exactly once in the output
|
||||
// and in the correct order after slicing.
|
||||
let batch = make_batch(237); // odd count to exercise remainder slice
|
||||
let bytes = batch.get_array_memory_size();
|
||||
let result = collect_rechunked(vec![batch], bytes / 8, bytes / 5);
|
||||
|
||||
assert_eq!(total_rows(&result), 237);
|
||||
|
||||
let values: Vec<i32> = result
|
||||
.iter()
|
||||
.flat_map(|b| {
|
||||
b.column(0)
|
||||
.as_any()
|
||||
.downcast_ref::<Int32Array>()
|
||||
.unwrap()
|
||||
.values()
|
||||
.iter()
|
||||
.copied()
|
||||
})
|
||||
.collect();
|
||||
let expected: Vec<i32> = (0..237).collect();
|
||||
assert_eq!(values, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_bytes_zero_still_yields_all_rows() {
|
||||
// When min_bytes=0, the stream should still yield every batch.
|
||||
// This is the "chop only, don't coalesce" use case.
|
||||
let batches: Vec<_> = (0..5).map(|_| make_batch(100)).collect();
|
||||
let batch_bytes = batches[0].get_array_memory_size();
|
||||
let result = collect_rechunked(batches, 0, batch_bytes * 2);
|
||||
assert_eq!(total_rows(&result), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_bytes_zero_slices_oversized() {
|
||||
// min_bytes=0 with a small max_bytes should still slice large batches.
|
||||
let batch = make_batch(1000);
|
||||
let bytes = batch.get_array_memory_size();
|
||||
let result = collect_rechunked(vec![batch], 0, bytes / 4);
|
||||
assert_eq!(total_rows(&result), 1000);
|
||||
assert!(
|
||||
result.len() >= 4,
|
||||
"expected at least 4 slices, got {}",
|
||||
result.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a batch with one variable-length string column.
|
||||
/// Every row is `small_size` bytes except the row at index `big_row_idx`
|
||||
/// which is `big_size` bytes.
|
||||
fn make_variable_batch(
|
||||
num_rows: usize,
|
||||
small_size: usize,
|
||||
big_row_idx: usize,
|
||||
big_size: usize,
|
||||
) -> RecordBatch {
|
||||
let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
|
||||
let values: Vec<String> = (0..num_rows)
|
||||
.map(|i| {
|
||||
if i == big_row_idx {
|
||||
"X".repeat(big_size)
|
||||
} else {
|
||||
"x".repeat(small_size)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let array = arrow_array::StringArray::from(values);
|
||||
RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap()
|
||||
}
|
||||
|
||||
fn variable_schema() -> SchemaRef {
|
||||
Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]))
|
||||
}
|
||||
|
||||
fn collect_rechunked_variable(
|
||||
batches: Vec<RecordBatch>,
|
||||
min_bytes: usize,
|
||||
max_bytes: usize,
|
||||
) -> Vec<RecordBatch> {
|
||||
let input = stream::iter(batches.into_iter().map(Ok::<_, ArrowError>));
|
||||
let rechunked =
|
||||
rechunk_stream_by_size_deep_copy(input, variable_schema(), min_bytes, max_bytes);
|
||||
block_on(rechunked.collect::<Vec<_>>())
|
||||
.into_iter()
|
||||
.map(|r| r.unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oversized_row_at_end() {
|
||||
// 100 rows: 99 small (64 bytes each) + 1 large (100KiB) at the end.
|
||||
let batch = make_variable_batch(100, 64, 99, 100 * 1024);
|
||||
let max_bytes = 64 * 1024;
|
||||
let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
|
||||
assert_eq!(total_rows(&result), 100);
|
||||
for (i, b) in result.iter().enumerate() {
|
||||
let size = b.get_array_memory_size();
|
||||
assert!(
|
||||
size <= max_bytes || b.num_rows() == 1,
|
||||
"batch {i} has {size} bytes (max {max_bytes}) and {} rows",
|
||||
b.num_rows()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oversized_row_at_start() {
|
||||
// 100 rows: 1 large (100KiB) at the start + 99 small (64 bytes each).
|
||||
let batch = make_variable_batch(100, 64, 0, 100 * 1024);
|
||||
let max_bytes = 64 * 1024;
|
||||
let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
|
||||
assert_eq!(total_rows(&result), 100);
|
||||
for (i, b) in result.iter().enumerate() {
|
||||
let size = b.get_array_memory_size();
|
||||
assert!(
|
||||
size <= max_bytes || b.num_rows() == 1,
|
||||
"batch {i} has {size} bytes (max {max_bytes}) and {} rows",
|
||||
b.num_rows()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oversized_row_in_middle() {
|
||||
// 100 rows: 1 large (100KiB) in the middle + 99 small (64 bytes each).
|
||||
let batch = make_variable_batch(100, 64, 50, 100 * 1024);
|
||||
let max_bytes = 64 * 1024;
|
||||
let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
|
||||
assert_eq!(total_rows(&result), 100);
|
||||
for (i, b) in result.iter().enumerate() {
|
||||
let size = b.get_array_memory_size();
|
||||
assert!(
|
||||
size <= max_bytes || b.num_rows() == 1,
|
||||
"batch {i} has {size} bytes (max {max_bytes}) and {} rows",
|
||||
b.num_rows()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_propagation() {
|
||||
let input = stream::iter(vec![
|
||||
Ok(make_batch(10)),
|
||||
Err(ArrowError::ComputeError("boom".into())),
|
||||
Ok(make_batch(10)),
|
||||
]);
|
||||
let rechunked = rechunk_stream_by_size(input, test_schema(), 1, usize::MAX);
|
||||
let results: Vec<Result<RecordBatch, ArrowError>> = block_on(rechunked.collect());
|
||||
assert!(results.iter().any(|r| r.is_err()));
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Extension to arrow struct arrays
|
||||
|
||||
use arrow_array::{Array, StructArray, cast::AsArray, make_array};
|
||||
use arrow_buffer::NullBuffer;
|
||||
use arrow_data::{ArrayData, ArrayDataBuilder};
|
||||
use arrow_schema::ArrowError;
|
||||
|
||||
pub trait StructArrayExt {
|
||||
/// Removes the offset / length of the struct array by pushing it into the children
|
||||
///
|
||||
/// In arrow-rs when slice is called it recursively slices the children.
|
||||
/// In arrow-cpp when slice is called it just sets the offset/length of
|
||||
/// the struct array and leaves the children as-is
|
||||
///
|
||||
/// Both are legal approaches (╥﹏╥)
|
||||
///
|
||||
/// This method helps reduce complexity by folding into the arrow-rs approach
|
||||
fn normalize_slicing(&self) -> Result<Self, ArrowError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Structs are allowed to mask valid items. For example, a struct array might be:
|
||||
///
|
||||
/// [ {"items": [1, 2, 3]}, NULL, {"items": NULL}]
|
||||
///
|
||||
/// However, the underlying items array might be: [[1, 2, 3], [4, 5], NULL]
|
||||
///
|
||||
/// The [4, 5] list is masked out because the struct array is null.
|
||||
///
|
||||
/// The struct validity would be [true, false, true] and the list validity would be [true, true, false]
|
||||
///
|
||||
/// This method pushes nulls down into all children. In the above example the list validity would become
|
||||
/// [true, false, false].
|
||||
///
|
||||
/// This method is not recursive. If a child is a struct array it will not push that child's nulls down.
|
||||
///
|
||||
/// This method does not remove garbage lists. It only updates the validity so a future call to
|
||||
/// [crate::list::ListArrayExt::filter_garbage_nulls] will remove the garbage lists (without
|
||||
/// this pushdown it would not)
|
||||
fn pushdown_nulls(&self) -> Result<Self, ArrowError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
fn normalized_struct_array_data(data: ArrayData) -> Result<ArrayData, ArrowError> {
|
||||
let parent_offset = data.offset();
|
||||
let parent_len = data.len();
|
||||
let modified_children = data
|
||||
.child_data()
|
||||
.iter()
|
||||
.map(|d| {
|
||||
let d = normalized_struct_array_data(d.clone())?;
|
||||
let offset = d.offset();
|
||||
let len = d.len();
|
||||
if len < parent_len + parent_offset {
|
||||
return Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Child array {} has length {} which is less than the parent length {} plus the parent offset {}",
|
||||
d.data_type(),
|
||||
len,
|
||||
parent_len,
|
||||
parent_offset
|
||||
)));
|
||||
}
|
||||
let new_offset = offset + parent_offset;
|
||||
d.into_builder().offset(new_offset)
|
||||
.len(parent_len)
|
||||
.build()
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
ArrayDataBuilder::new(data.data_type().clone())
|
||||
.len(parent_len)
|
||||
.offset(0)
|
||||
.buffers(data.buffers().to_vec())
|
||||
.child_data(modified_children)
|
||||
.build()
|
||||
}
|
||||
|
||||
impl StructArrayExt for StructArray {
|
||||
fn normalize_slicing(&self) -> Result<Self, ArrowError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if self.offset() == 0 && self.columns().iter().all(|c| c.len() == self.len()) {
|
||||
return Ok(self.clone());
|
||||
}
|
||||
|
||||
let data = normalized_struct_array_data(self.to_data())?;
|
||||
Ok(Self::from(data))
|
||||
}
|
||||
|
||||
fn pushdown_nulls(&self) -> Result<Self, ArrowError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let Some(validity) = self.nulls() else {
|
||||
return Ok(self.clone());
|
||||
};
|
||||
let data = self.to_data();
|
||||
let children = data
|
||||
.child_data()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
if let Some(child_validity) = c.nulls() {
|
||||
let new_validity = child_validity.inner() & validity.inner();
|
||||
c.clone()
|
||||
.into_builder()
|
||||
.nulls(Some(NullBuffer::from(new_validity)))
|
||||
.build()
|
||||
} else {
|
||||
Ok(c.clone()
|
||||
.into_builder()
|
||||
.nulls(Some(validity.clone()))
|
||||
.build()?)
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let arr = make_array(data.into_builder().child_data(children).build()?);
|
||||
Ok(arr.as_struct().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use arrow_array::{Array, Int32Array, StructArray, cast::AsArray, make_array};
|
||||
use arrow_schema::{DataType, Field, Fields};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::r#struct::StructArrayExt;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_slicing_no_offset() {
|
||||
let x = Int32Array::from(vec![1, 2, 3]);
|
||||
let y = Int32Array::from(vec![4, 5, 6]);
|
||||
let struct_array = StructArray::new(
|
||||
Fields::from(vec![
|
||||
Field::new("x", DataType::Int32, true),
|
||||
Field::new("y", DataType::Int32, true),
|
||||
]),
|
||||
vec![Arc::new(x), Arc::new(y)],
|
||||
None,
|
||||
);
|
||||
|
||||
let normalized = struct_array.normalize_slicing().unwrap();
|
||||
assert_eq!(normalized, struct_array);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arrow_rs_slicing() {
|
||||
let x = Int32Array::from(vec![1, 2, 3, 4]);
|
||||
let y = Int32Array::from(vec![5, 6, 7, 8]);
|
||||
let struct_array = StructArray::new(
|
||||
Fields::from(vec![
|
||||
Field::new("x", DataType::Int32, true),
|
||||
Field::new("y", DataType::Int32, true),
|
||||
]),
|
||||
vec![Arc::new(x), Arc::new(y)],
|
||||
None,
|
||||
);
|
||||
|
||||
// Slicing with arrow-rs propagates the slicing to the children so there should
|
||||
// be no change needed to the struct array
|
||||
let sliced = struct_array.slice(1, 2);
|
||||
let normalized = sliced.normalize_slicing().unwrap();
|
||||
|
||||
assert_eq!(normalized, sliced);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arrow_cpp_slicing() {
|
||||
let x = Int32Array::from(vec![1, 2, 3, 4]);
|
||||
let y = Int32Array::from(vec![5, 6, 7, 8]);
|
||||
let struct_array = StructArray::new(
|
||||
Fields::from(vec![
|
||||
Field::new("x", DataType::Int32, true),
|
||||
Field::new("y", DataType::Int32, true),
|
||||
]),
|
||||
vec![Arc::new(x), Arc::new(y)],
|
||||
None,
|
||||
);
|
||||
|
||||
let data = struct_array.to_data();
|
||||
let sliced = data.into_builder().offset(1).len(2).build().unwrap();
|
||||
let sliced = make_array(sliced);
|
||||
let normalized = sliced.as_struct().clone().normalize_slicing().unwrap();
|
||||
|
||||
assert_eq!(normalized, struct_array.slice(1, 2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{Array, ArrayRef, BooleanArray, Int32Array, RecordBatch, StructArray};
|
||||
use arrow_buffer::NullBuffer;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
use lance_arrow::RecordBatchExt;
|
||||
|
||||
#[test]
|
||||
fn merge_nullable_struct_with_sliced_boolean_child() {
|
||||
let left_struct = StructArray::new(
|
||||
vec![Field::new("id", DataType::Int32, false)].into(),
|
||||
vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as ArrayRef],
|
||||
None,
|
||||
);
|
||||
|
||||
// BooleanArray retains its offset when sliced. The merged child bitmap must
|
||||
// cover that offset as well as the four visible rows.
|
||||
let boolean_child: ArrayRef = Arc::new(BooleanArray::from_iter((0..1030).map(|index| {
|
||||
if index == 1027 {
|
||||
None
|
||||
} else {
|
||||
Some(index % 2 == 1)
|
||||
}
|
||||
})));
|
||||
let boolean_child = boolean_child.slice(1025, 4);
|
||||
assert_eq!(boolean_child.offset(), 1025);
|
||||
let right_struct = StructArray::new(
|
||||
vec![Field::new("flag", DataType::Boolean, true)].into(),
|
||||
vec![boolean_child],
|
||||
Some(NullBuffer::from(vec![true, false, true, true])),
|
||||
);
|
||||
|
||||
let left_batch = RecordBatch::try_new(
|
||||
Arc::new(Schema::new(vec![Field::new(
|
||||
"nested",
|
||||
left_struct.data_type().clone(),
|
||||
true,
|
||||
)])),
|
||||
vec![Arc::new(left_struct)],
|
||||
)
|
||||
.unwrap();
|
||||
let right_batch = RecordBatch::try_new(
|
||||
Arc::new(Schema::new(vec![Field::new(
|
||||
"nested",
|
||||
right_struct.data_type().clone(),
|
||||
true,
|
||||
)])),
|
||||
vec![Arc::new(right_struct)],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let merged = left_batch.merge(&right_batch).unwrap();
|
||||
let nested = merged.column_by_name("nested").unwrap();
|
||||
let nested = nested.as_any().downcast_ref::<StructArray>().unwrap();
|
||||
let flag = nested.column_by_name("flag").unwrap();
|
||||
let flag = flag.as_any().downcast_ref::<BooleanArray>().unwrap();
|
||||
assert_eq!(
|
||||
flag.iter().collect::<Vec<_>>(),
|
||||
vec![Some(true), None, None, Some(false)]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user