@@ -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