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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions include/lance/lance.h
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,38 @@ int32_t lance_scanner_set_include_deleted_rows(
bool include_deleted_rows
);

/** How blob columns are materialized by a scan. Validated as an integer. */
typedef enum {
/**
* Default: blob columns are returned as descriptor structs and every
* other binary column is returned as bytes. The descriptor layout
* depends on the storage format of the column: Blob v2 columns yield
* (kind, position, size, blob_id, blob_uri), while legacy blob columns
* (large_binary tagged `lance-encoding: blob`) yield (position, size).
*/
LANCE_BLOB_HANDLING_BLOBS_DESCRIPTIONS = 0,
/** Every blob column is materialized as bytes (LargeBinary). */
LANCE_BLOB_HANDLING_ALL_BINARY = 1,
/**
* Requests descriptors for every binary column. On lance v11.0.0 only
* columns carrying blob metadata are affected; other binary columns keep
* their bytes, so this behaves like
* LANCE_BLOB_HANDLING_BLOBS_DESCRIPTIONS.
*/
LANCE_BLOB_HANDLING_ALL_DESCRIPTIONS = 2,
} LanceBlobHandling;

/**
* Choose how blob columns are materialized by this scan. Default:
* LANCE_BLOB_HANDLING_BLOBS_DESCRIPTIONS. ALL_BINARY pulls the full payload
* into the batches, so keep descriptors for large values. Columns without
* blob metadata keep their bytes under every mode.
*
* Must be set before scanning starts; values outside the enum are rejected.
* @return 0 on success, -1 on error
*/
int32_t lance_scanner_set_blob_handling(LanceScanner* scanner, LanceBlobHandling handling);

/**
* Restrict scan to the given fragment IDs. Must be called before iteration.
* @param ids Array of fragment IDs
Expand Down
8 changes: 8 additions & 0 deletions include/lance/lance.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,14 @@ class Scanner {
return *this;
}

/// Choose how blob columns are materialized (default: descriptors for blob
/// columns, bytes for every other binary column).
Scanner& blob_handling(LanceBlobHandling handling) {
if (lance_scanner_set_blob_handling(handle_.get(), handling) != 0)
check_error();
return *this;
}

/// Enable/disable row ID in output.
Scanner& with_row_id(bool enable = true) {
if (lance_scanner_with_row_id(handle_.get(), enable) != 0)
Expand Down
88 changes: 88 additions & 0 deletions src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use lance::dataset::scanner::{
};
use lance::io::exec::fts::{FlatMatchQueryExec, MatchQueryExec, PhraseQueryExec};
use lance_core::Result;
use lance_core::datatypes::BlobHandling;
use lance_index::scalar::FullTextSearchQuery;
use lance_index::vector::ApproxMode;
use lance_io::stream::RecordBatchStream;
Expand Down Expand Up @@ -92,6 +93,7 @@ pub struct LanceScanner {
filter: Option<String>,
substrait_filter: Option<Vec<u8>>,
additional_sql_filters: Vec<String>,
blob_handling: Option<BlobHandling>,
limit: Option<i64>,
offset: Option<i64>,
batch_size: Option<usize>,
Expand Down Expand Up @@ -241,6 +243,7 @@ impl LanceScanner {
filter: None,
substrait_filter: None,
additional_sql_filters: Vec::new(),
blob_handling: None,
limit: None,
offset: None,
batch_size: None,
Expand Down Expand Up @@ -363,6 +366,9 @@ impl LanceScanner {
if let Some(cols) = &self.columns {
scanner.project(cols)?;
}
if let Some(handling) = &self.blob_handling {
scanner.blob_handling(handling.clone());
}
let multi_vector = self.nearest.as_ref().is_some_and(|query| {
matches!(
query.query.data_type(),
Expand Down Expand Up @@ -1390,6 +1396,46 @@ unsafe fn scanner_set_use_stats_inner(scanner: *mut LanceScanner, use_stats: boo
Ok(0)
}

/// Set how blob columns are materialized. `handling` is the C enum
/// `LanceBlobHandling` as an integer: 0 descriptors for blob columns (the
/// default), 1 bytes for every blob column, 2 descriptors for every binary
/// column. Other values are rejected. Must be set before the scan starts.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lance_scanner_set_blob_handling(
scanner: *mut LanceScanner,
handling: i32,
) -> i32 {
scanner_poison_check!(scanner, -1);
scanner_ffi_try!(scanner, unsafe {
scanner_set_blob_handling_inner(scanner, handling)
})
}

unsafe fn scanner_set_blob_handling_inner(
scanner: *mut LanceScanner,
handling: i32,
) -> Result<i32> {
if scanner.is_null() {
return Err(lance_core::Error::invalid_input_source(
"scanner is NULL".into(),
));
}
let scanner = unsafe { &mut *scanner };
scanner.ensure_scan_not_started("blob_handling")?;
let parsed = match handling {
0 => BlobHandling::BlobsDescriptions,
1 => BlobHandling::AllBinary,
2 => BlobHandling::AllDescriptions,
other => {
return Err(lance_core::Error::invalid_input(format!(
"blob_handling must be 0 (blobs as descriptions), 1 (all binary) or 2 (all descriptions); got {other}"
)));
}
};
scanner.blob_handling = Some(parsed);
Ok(0)
}

/// Enable or disable row ID in scan output. Returns 0.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lance_scanner_with_row_id(
Expand Down Expand Up @@ -3369,6 +3415,48 @@ mod tests {
}
}

#[test]
fn set_blob_handling_stores_the_matching_upstream_variant() {
// A scan cannot tell AllDescriptions from BlobsDescriptions on lance
// v11, so check the stored variant directly.
let (_tmp, uri) = create_test_dataset();
let (dataset, scanner) = open_dataset_and_scanner(&uri);

assert_eq!(
unsafe { &*scanner }.blob_handling,
None,
"blob handling should be unset until the setter is called"
);

for (handling, expected) in [
(0, BlobHandling::BlobsDescriptions),
(1, BlobHandling::AllBinary),
(2, BlobHandling::AllDescriptions),
] {
assert_eq!(
unsafe { lance_scanner_set_blob_handling(scanner, handling) },
0
);
assert_eq!(
unsafe { &*scanner }.blob_handling,
Some(expected),
"blob handling {handling} stored the wrong variant"
);
}

// A rejected value leaves the last accepted mode in place.
assert_eq!(unsafe { lance_scanner_set_blob_handling(scanner, 3) }, -1);
assert_eq!(
unsafe { &*scanner }.blob_handling,
Some(BlobHandling::AllDescriptions)
);

unsafe {
lance_scanner_close(scanner);
lance_dataset_close(dataset);
}
}

#[test]
fn null_poll_waker_is_rejected_and_clears_out() {
let (_tmp, uri) = create_test_dataset();
Expand Down
Loading
Loading