Skip to content

Commit c485eb4

Browse files
author
niuyulin
committed
feat: support merge_insert with blob encoding column
1 parent d217a02 commit c485eb4

2 files changed

Lines changed: 143 additions & 22 deletions

File tree

rust/lance/src/datafusion/dataframe.rs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub struct LanceTableProvider {
4242
row_id_idx: Option<usize>,
4343
row_addr_idx: Option<usize>,
4444
ordered: bool,
45+
blob_handling: Option<lance_core::datatypes::BlobHandling>,
4546
}
4647

4748
impl LanceTableProvider {
@@ -72,9 +73,15 @@ impl LanceTableProvider {
7273
row_id_idx,
7374
row_addr_idx,
7475
ordered,
76+
blob_handling: None,
7577
}
7678
}
7779

80+
pub fn with_blob_handling(mut self, handling: lance_core::datatypes::BlobHandling) -> Self {
81+
self.blob_handling = Some(handling);
82+
self
83+
}
84+
7885
pub fn dataset(&self) -> Arc<Dataset> {
7986
self.dataset.clone()
8087
}
@@ -102,6 +109,10 @@ impl TableProvider for LanceTableProvider {
102109
limit: Option<usize>,
103110
) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
104111
let mut scan = self.dataset.scan();
112+
if let Some(handling) = self.blob_handling.clone() {
113+
scan.blob_handling(handling);
114+
}
115+
105116
match projection {
106117
Some(projection) if projection.is_empty() => {
107118
scan.empty_project()?;
@@ -166,13 +177,7 @@ pub trait SessionContextExt {
166177
with_row_id: bool,
167178
with_row_addr: bool,
168179
) -> datafusion::common::Result<DataFrame>;
169-
/// Creates a DataFrame for reading a Lance dataset without ordering
170-
fn read_lance_unordered(
171-
&self,
172-
dataset: Arc<Dataset>,
173-
with_row_id: bool,
174-
with_row_addr: bool,
175-
) -> datafusion::common::Result<DataFrame>;
180+
176181
/// Creates a DataFrame for reading a stream of data
177182
///
178183
/// This dataframe may only be queried once, future queries will fail
@@ -232,20 +237,6 @@ impl SessionContextExt for SessionContext {
232237
)))
233238
}
234239

235-
fn read_lance_unordered(
236-
&self,
237-
dataset: Arc<Dataset>,
238-
with_row_id: bool,
239-
with_row_addr: bool,
240-
) -> datafusion::common::Result<DataFrame> {
241-
self.read_table(Arc::new(LanceTableProvider::new_with_ordering(
242-
dataset,
243-
with_row_id,
244-
with_row_addr,
245-
false,
246-
)))
247-
}
248-
249240
fn read_one_shot(
250241
&self,
251242
data: SendableRecordBatchStream,

rust/lance/src/dataset/write/merge_insert.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1658,7 +1658,16 @@ impl MergeInsertJob {
16581658
// indexed vs non-indexed cases. That should be handled by optimizer rules.
16591659
let session_config = SessionConfig::default();
16601660
let session_ctx = SessionContext::new_with_config(session_config);
1661-
let scan = session_ctx.read_lance_unordered(self.dataset.clone(), true, true)?;
1661+
let provider = Arc::new(
1662+
crate::datafusion::dataframe::LanceTableProvider::new_with_ordering(
1663+
self.dataset.clone(),
1664+
true,
1665+
true,
1666+
false,
1667+
)
1668+
.with_blob_handling(lance_core::datatypes::BlobHandling::AllBinary),
1669+
);
1670+
let scan = session_ctx.read_table(provider)?;
16621671
// Wrap column names in double quotes to preserve case (DataFusion lowercases unquoted identifiers)
16631672
let on_cols = self
16641673
.params
@@ -11448,4 +11457,125 @@ MergeInsert: on=[id], when_matched=DoNothing, when_not_matched=InsertAll, when_n
1144811457
}
1144911458
}
1145011459
}
11460+
#[tokio::test]
11461+
async fn test_merge_insert_with_blob() {
11462+
use arrow_array::LargeBinaryArray;
11463+
use arrow_schema::Field;
11464+
use arrow_schema::Schema as ArrowSchema;
11465+
use lance_arrow::BLOB_META_KEY;
11466+
11467+
let test_dir = TempStrDir::default();
11468+
let blob_meta = HashMap::from([(BLOB_META_KEY.to_string(), "true".to_string())]);
11469+
let schema = Arc::new(ArrowSchema::new(vec![
11470+
Field::new("blobs", DataType::LargeBinary, true).with_metadata(blob_meta.clone()),
11471+
Field::new("id", DataType::Int64, true),
11472+
Field::new("other", DataType::Int64, true),
11473+
]));
11474+
11475+
let batch = RecordBatch::try_new(
11476+
schema.clone(),
11477+
vec![
11478+
Arc::new(LargeBinaryArray::from(vec![
11479+
Some(b"foo".as_slice()),
11480+
Some(b"bar".as_slice()),
11481+
])),
11482+
Arc::new(Int64Array::from(vec![0, 1])),
11483+
Arc::new(Int64Array::from(vec![10, 20])),
11484+
],
11485+
)
11486+
.unwrap();
11487+
11488+
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
11489+
let dataset = Dataset::write(
11490+
reader,
11491+
&test_dir,
11492+
Some(WriteParams {
11493+
data_storage_version: Some(LanceFileVersion::V2_1),
11494+
..Default::default()
11495+
}),
11496+
)
11497+
.await
11498+
.unwrap();
11499+
11500+
// Now update without providing 'blobs'
11501+
let source_schema = Arc::new(ArrowSchema::new(vec![
11502+
Field::new("id", DataType::Int64, true),
11503+
Field::new("other", DataType::Int64, true),
11504+
]));
11505+
let new_data = RecordBatch::try_new(
11506+
source_schema.clone(),
11507+
vec![
11508+
Arc::new(Int64Array::from(vec![1, 2])),
11509+
Arc::new(Int64Array::from(vec![200, 300])),
11510+
],
11511+
)
11512+
.unwrap();
11513+
let new_reader = Box::new(RecordBatchIterator::new(
11514+
vec![Ok(new_data)],
11515+
source_schema.clone(),
11516+
));
11517+
11518+
let job = MergeInsertBuilder::try_new(Arc::new(dataset), vec!["id".to_string()])
11519+
.unwrap()
11520+
.when_matched(WhenMatched::UpdateAll)
11521+
.when_not_matched(WhenNotMatched::InsertAll)
11522+
.try_build()
11523+
.unwrap();
11524+
11525+
// This will crash due to LargeBinary vs Struct schema mismatch!
11526+
let result = job.execute_reader(new_reader).await;
11527+
// With AllBinary set, the partial update should succeed!
11528+
let (new_dataset, _stats) = result.unwrap();
11529+
assert_eq!(new_dataset.count_rows(None).await.unwrap(), 3);
11530+
11531+
let batches = new_dataset
11532+
.scan()
11533+
.try_into_stream()
11534+
.await
11535+
.unwrap()
11536+
.try_collect::<Vec<_>>()
11537+
.await
11538+
.unwrap();
11539+
let batch = arrow_select::concat::concat_batches(&batches[0].schema(), &batches).unwrap();
11540+
11541+
let id_col = batch
11542+
.column_by_name("id")
11543+
.unwrap()
11544+
.as_any()
11545+
.downcast_ref::<Int64Array>()
11546+
.unwrap();
11547+
let other_col = batch
11548+
.column_by_name("other")
11549+
.unwrap()
11550+
.as_any()
11551+
.downcast_ref::<Int64Array>()
11552+
.unwrap();
11553+
let blobs_col = batch
11554+
.column_by_name("blobs")
11555+
.unwrap()
11556+
.as_any()
11557+
.downcast_ref::<arrow_array::StructArray>()
11558+
.unwrap();
11559+
11560+
for i in 0..batch.num_rows() {
11561+
let id = id_col.value(i);
11562+
let other = other_col.value(i);
11563+
let blob_is_null = blobs_col.is_null(i);
11564+
match id {
11565+
0 => {
11566+
assert_eq!(other, 10);
11567+
assert!(!blob_is_null, "id=0 should have kept its blob");
11568+
}
11569+
1 => {
11570+
assert_eq!(other, 200);
11571+
assert!(!blob_is_null, "id=1 should have kept its blob on update");
11572+
}
11573+
2 => {
11574+
assert_eq!(other, 300);
11575+
assert!(blob_is_null, "id=2 should have a null blob on insert");
11576+
}
11577+
_ => panic!("Unexpected id: {}", id),
11578+
}
11579+
}
11580+
}
1145111581
}

0 commit comments

Comments
 (0)