Files
lancedb/python/vendor/lance-arrow/src/schema.rs
T
lancedb-gatefixer[bot] 624defd109 fix: preserve sliced child validity during struct merge (#4261)
## Root cause

The pinned Lance Arrow crate rebuilt a sliced struct child with its
original Arrow offset but supplied a validity bitmap containing only the
visible rows. Arrow requires the bitmap to cover the offset as well, so
filtered vector searches involving nullable nested fields could panic
with `null_bit_buffer size too small`.

## Fix

- Carry an offset-aligned validity bitmap when propagating parent nulls
to a sliced child.
- Vendor the pinned `lance-arrow` crate until a Lance release includes
the fix; include it in Maturin source distributions.
- Add unit regressions for sliced children and parent bitmaps, plus a
record-batch merge regression.

## Validation

- The issue's 8,600-row Python reproducer completes its filtered flat
dot search and returns 8,448 rows without a panic.
- The targeted Rust merge regression and two focused Python query tests
pass.
- Rust formatting, Ruff formatting and lint, the vendored crate tests,
and the workspace Cargo checks passed. The Maturin source archive
includes the patched crate.

Fixes #4260

<!-- lance-gatekeeper-fix:v1 agent=e6045f701d9b708ee26528ba36353aba
generation=1 -->

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-10-23 01:26:28 +08:00

209 lines
6.8 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
// 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
}
}