Skip to content

Commit 34389ac

Browse files
author
niuyulin
committed
feat: support merge_insert with blob encoding column
1 parent 45eb977 commit 34389ac

2 files changed

Lines changed: 145 additions & 21 deletions

File tree

rust/lance/src/datafusion/dataframe.rs

Lines changed: 13 additions & 20 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,19 +237,7 @@ 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-
}
240+
248241

249242
fn read_one_shot(
250243
&self,

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

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,16 @@ impl MergeInsertJob {
14671467
// indexed vs non-indexed cases. That should be handled by optimizer rules.
14681468
let session_config = SessionConfig::default();
14691469
let session_ctx = SessionContext::new_with_config(session_config);
1470-
let scan = session_ctx.read_lance_unordered(self.dataset.clone(), true, true)?;
1470+
let provider = Arc::new(
1471+
crate::datafusion::dataframe::LanceTableProvider::new_with_ordering(
1472+
self.dataset.clone(),
1473+
true,
1474+
true,
1475+
false,
1476+
)
1477+
.with_blob_handling(lance_core::datatypes::BlobHandling::AllBinary),
1478+
);
1479+
let scan = session_ctx.read_table(provider)?;
14711480
// Wrap column names in double quotes to preserve case (DataFusion lowercases unquoted identifiers)
14721481
let on_cols = self
14731482
.params
@@ -10922,4 +10931,126 @@ MergeInsert: on=[id], when_matched=DoNothing, when_not_matched=InsertAll, when_n
1092210931
"Newly written merge-insert data files should be cleaned up on apply_deletions failure"
1092310932
);
1092410933
}
10934+
#[tokio::test]
10935+
async fn test_merge_insert_with_blob() {
10936+
use arrow_array::LargeBinaryArray;
10937+
use arrow_schema::Field;
10938+
use arrow_schema::Schema as ArrowSchema;
10939+
use lance_arrow::BLOB_META_KEY;
10940+
10941+
let test_dir = TempStrDir::default();
10942+
let blob_meta = HashMap::from([(BLOB_META_KEY.to_string(), "true".to_string())]);
10943+
let schema = Arc::new(ArrowSchema::new(vec![
10944+
Field::new("blobs", DataType::LargeBinary, true).with_metadata(blob_meta.clone()),
10945+
Field::new("id", DataType::Int64, true),
10946+
Field::new("other", DataType::Int64, true),
10947+
]));
10948+
10949+
let batch = RecordBatch::try_new(
10950+
schema.clone(),
10951+
vec![
10952+
Arc::new(LargeBinaryArray::from(vec![
10953+
Some(b"foo".as_slice()),
10954+
Some(b"bar".as_slice()),
10955+
])),
10956+
Arc::new(Int64Array::from(vec![0, 1])),
10957+
Arc::new(Int64Array::from(vec![10, 20])),
10958+
],
10959+
)
10960+
.unwrap();
10961+
10962+
let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone());
10963+
let dataset = Dataset::write(
10964+
reader,
10965+
&test_dir,
10966+
Some(WriteParams {
10967+
data_storage_version: Some(LanceFileVersion::V2_1),
10968+
..Default::default()
10969+
}),
10970+
)
10971+
.await
10972+
.unwrap();
10973+
10974+
// Now update without providing 'blobs'
10975+
let source_schema = Arc::new(ArrowSchema::new(vec![
10976+
Field::new("id", DataType::Int64, true),
10977+
Field::new("other", DataType::Int64, true),
10978+
]));
10979+
let new_data = RecordBatch::try_new(
10980+
source_schema.clone(),
10981+
vec![
10982+
Arc::new(Int64Array::from(vec![1, 2])),
10983+
Arc::new(Int64Array::from(vec![200, 300])),
10984+
],
10985+
)
10986+
.unwrap();
10987+
let new_reader = Box::new(RecordBatchIterator::new(
10988+
vec![Ok(new_data)],
10989+
source_schema.clone(),
10990+
));
10991+
10992+
let job = MergeInsertBuilder::try_new(Arc::new(dataset), vec!["id".to_string()])
10993+
.unwrap()
10994+
.when_matched(WhenMatched::UpdateAll)
10995+
.when_not_matched(WhenNotMatched::InsertAll)
10996+
.try_build()
10997+
.unwrap();
10998+
10999+
// This will crash due to LargeBinary vs Struct schema mismatch!
11000+
let result = job.execute_reader(new_reader).await;
11001+
// With AllBinary set, the partial update should succeed!
11002+
let (new_dataset, _stats) = result.unwrap();
11003+
assert_eq!(new_dataset.count_rows(None).await.unwrap(), 3);
11004+
11005+
let batches = new_dataset
11006+
.scan()
11007+
.try_into_stream()
11008+
.await
11009+
.unwrap()
11010+
.try_collect::<Vec<_>>()
11011+
.await
11012+
.unwrap();
11013+
let batch = arrow_select::concat::concat_batches(&batches[0].schema(), &batches).unwrap();
11014+
11015+
let id_col = batch
11016+
.column_by_name("id")
11017+
.unwrap()
11018+
.as_any()
11019+
.downcast_ref::<Int64Array>()
11020+
.unwrap();
11021+
let other_col = batch
11022+
.column_by_name("other")
11023+
.unwrap()
11024+
.as_any()
11025+
.downcast_ref::<Int64Array>()
11026+
.unwrap();
11027+
let blobs_col = batch
11028+
.column_by_name("blobs")
11029+
.unwrap()
11030+
.as_any()
11031+
.downcast_ref::<arrow_array::StructArray>()
11032+
.unwrap();
11033+
11034+
for i in 0..batch.num_rows() {
11035+
let id = id_col.value(i);
11036+
let other = other_col.value(i);
11037+
let blob_is_null = blobs_col.is_null(i);
11038+
match id {
11039+
0 => {
11040+
assert_eq!(other, 10);
11041+
assert!(!blob_is_null, "id=0 should have kept its blob");
11042+
}
11043+
1 => {
11044+
assert_eq!(other, 200);
11045+
assert!(!blob_is_null, "id=1 should have kept its blob on update");
11046+
}
11047+
2 => {
11048+
assert_eq!(other, 300);
11049+
assert!(blob_is_null, "id=2 should have a null blob on insert");
11050+
}
11051+
_ => panic!("Unexpected id: {}", id),
11052+
}
11053+
}
11054+
}
1092511055
}
11056+

0 commit comments

Comments
 (0)