@@ -22,18 +22,20 @@ use arrow::datatypes::SchemaRef;
2222use async_ffi:: { FfiFuture , FutureExt } ;
2323use async_trait:: async_trait;
2424use datafusion_catalog:: { Session , TableProvider } ;
25- use datafusion_common:: Statistics ;
2625use datafusion_common:: error:: { DataFusionError , Result } ;
26+ use datafusion_common:: { DFSchemaRef , Statistics } ;
2727use datafusion_execution:: TaskContext ;
28- use datafusion_expr:: dml:: InsertOp ;
28+ use datafusion_expr:: dml:: { InsertOp , MergeIntoClause , MergeIntoOp } ;
2929use datafusion_expr:: { Expr , TableProviderFilterPushDown , TableType } ;
3030use datafusion_physical_plan:: ExecutionPlan ;
31- use datafusion_proto:: logical_plan:: from_proto:: parse_exprs;
32- use datafusion_proto:: logical_plan:: to_proto:: serialize_exprs;
31+ use datafusion_proto:: logical_plan:: from_proto:: { parse_exprs, parse_merge_into_op} ;
32+ use datafusion_proto:: logical_plan:: to_proto:: {
33+ serialize_exprs, serialize_merge_into_op,
34+ } ;
3335use datafusion_proto:: logical_plan:: {
3436 DefaultLogicalExtensionCodec , LogicalExtensionCodec ,
3537} ;
36- use datafusion_proto:: protobuf:: LogicalExprList ;
38+ use datafusion_proto:: protobuf:: { DfSchema , LogicalExprList , MergeIntoOpNode } ;
3739use prost:: Message ;
3840
3941use stabby:: vec:: Vec as SVec ;
@@ -135,6 +137,17 @@ pub struct FFI_TableProvider {
135137 insert_op : FFI_InsertOp ,
136138 ) -> FfiFuture < FFI_Result < FFI_ExecutionPlan > > ,
137139
140+ /// Merge source rows into the table. The schema and MERGE operation are
141+ /// prost-encoded to preserve logical qualifiers and expressions across the
142+ /// FFI boundary.
143+ merge_into : unsafe extern "C" fn (
144+ provider : & Self ,
145+ session : FFI_SessionRef ,
146+ source : & FFI_ExecutionPlan ,
147+ merge_schema_serialized : SVec < u8 > ,
148+ merge_op_serialized : SVec < u8 > ,
149+ ) -> FfiFuture < FFI_Result < FFI_ExecutionPlan > > ,
150+
138151 /// Snapshot the provider's table-level statistics. [`FFI_Option::None`]
139152 /// corresponds to [`TableProvider::statistics`] returning `None`;
140153 /// `Some(bytes)` is a prost-encoded `datafusion_proto_common::Statistics`.
@@ -336,6 +349,60 @@ unsafe extern "C" fn insert_into_fn_wrapper(
336349 . into_ffi ( )
337350}
338351
352+ unsafe extern "C" fn merge_into_fn_wrapper (
353+ provider : & FFI_TableProvider ,
354+ session : FFI_SessionRef ,
355+ source : & FFI_ExecutionPlan ,
356+ merge_schema_serialized : SVec < u8 > ,
357+ merge_op_serialized : SVec < u8 > ,
358+ ) -> FfiFuture < FFI_Result < FFI_ExecutionPlan > > {
359+ let task_ctx: Result < Arc < TaskContext > , DataFusionError > =
360+ ( & provider. logical_codec . task_ctx_provider ) . try_into ( ) ;
361+ let runtime = provider. runtime ( ) . clone ( ) ;
362+ let logical_codec: Arc < dyn LogicalExtensionCodec > = ( & provider. logical_codec ) . into ( ) ;
363+ let internal_provider = Arc :: clone ( provider. inner ( ) ) ;
364+ let source = source. clone ( ) ;
365+
366+ async move {
367+ let mut foreign_session = None ;
368+ let session = sresult_return ! (
369+ session
370+ . as_local( )
371+ . map( Ok :: <& ( dyn Session + Send + Sync ) , DataFusionError >)
372+ . unwrap_or_else( || {
373+ foreign_session = Some ( ForeignSession :: try_from( & session) ?) ;
374+ Ok ( foreign_session. as_ref( ) . unwrap( ) )
375+ } )
376+ ) ;
377+
378+ let source = sresult_return ! ( <Arc <dyn ExecutionPlan >>:: try_from( & source) ) ;
379+ let task_ctx = sresult_return ! ( task_ctx) ;
380+
381+ let merge_schema =
382+ sresult_return ! ( DfSchema :: decode( merge_schema_serialized. as_ref( ) ) ) ;
383+ let merge_schema: DFSchemaRef = sresult_return ! (
384+ DFSchemaRef :: try_from( merge_schema)
385+ . map_err( |e| DataFusionError :: Plan ( e. to_string( ) ) )
386+ ) ;
387+
388+ let merge_op =
389+ sresult_return ! ( MergeIntoOpNode :: decode( merge_op_serialized. as_ref( ) ) ) ;
390+ let MergeIntoOp { on, clauses } = sresult_return ! (
391+ parse_merge_into_op( & merge_op, task_ctx. as_ref( ) , logical_codec. as_ref( ) )
392+ . map_err( |e| DataFusionError :: Plan ( e. to_string( ) ) )
393+ ) ;
394+
395+ let plan = sresult_return ! (
396+ internal_provider
397+ . merge_into( session, source, merge_schema, on, clauses)
398+ . await
399+ ) ;
400+
401+ FFI_Result :: Ok ( FFI_ExecutionPlan :: new ( plan, runtime. clone ( ) ) )
402+ }
403+ . into_ffi ( )
404+ }
405+
339406unsafe extern "C" fn release_fn_wrapper ( provider : & mut FFI_TableProvider ) {
340407 unsafe {
341408 debug_assert ! ( !provider. private_data. is_null( ) ) ;
@@ -361,6 +428,7 @@ unsafe extern "C" fn clone_fn_wrapper(provider: &FFI_TableProvider) -> FFI_Table
361428 table_type : table_type_fn_wrapper,
362429 supports_filters_pushdown : provider. supports_filters_pushdown ,
363430 insert_into : provider. insert_into ,
431+ merge_into : provider. merge_into ,
364432 statistics : statistics_fn_wrapper,
365433 logical_codec : provider. logical_codec . clone ( ) ,
366434 clone : clone_fn_wrapper,
@@ -422,6 +490,7 @@ impl FFI_TableProvider {
422490 false => None ,
423491 } ,
424492 insert_into : insert_into_fn_wrapper,
493+ merge_into : merge_into_fn_wrapper,
425494 statistics : statistics_fn_wrapper,
426495 logical_codec,
427496 clone : clone_fn_wrapper,
@@ -577,16 +646,63 @@ impl TableProvider for ForeignTableProvider {
577646
578647 Ok ( plan)
579648 }
649+
650+ async fn merge_into (
651+ & self ,
652+ session : & dyn Session ,
653+ source : Arc < dyn ExecutionPlan > ,
654+ merge_schema : DFSchemaRef ,
655+ on : Expr ,
656+ clauses : Vec < MergeIntoClause > ,
657+ ) -> Result < Arc < dyn ExecutionPlan > > {
658+ let session = FFI_SessionRef :: new ( session, None , self . 0 . logical_codec . clone ( ) ) ;
659+
660+ let rc = Handle :: try_current ( ) . ok ( ) ;
661+ let source = FFI_ExecutionPlan :: new ( source, rc) ;
662+
663+ let merge_schema = DfSchema :: try_from ( & merge_schema)
664+ . map_err ( |e| DataFusionError :: Plan ( e. to_string ( ) ) ) ?;
665+ let merge_schema_serialized: SVec < u8 > =
666+ merge_schema. encode_to_vec ( ) . into_iter ( ) . collect ( ) ;
667+
668+ let codec: Arc < dyn LogicalExtensionCodec > = ( & self . 0 . logical_codec ) . into ( ) ;
669+ let merge_op =
670+ serialize_merge_into_op ( & MergeIntoOp { on, clauses } , codec. as_ref ( ) )
671+ . map_err ( |e| DataFusionError :: Plan ( e. to_string ( ) ) ) ?;
672+ let merge_op_serialized = merge_op. encode_to_vec ( ) . into_iter ( ) . collect ( ) ;
673+
674+ let plan = unsafe {
675+ let maybe_plan = ( self . 0 . merge_into ) (
676+ & self . 0 ,
677+ session,
678+ & source,
679+ merge_schema_serialized,
680+ merge_op_serialized,
681+ )
682+ . await ;
683+
684+ <Arc < dyn ExecutionPlan > >:: try_from ( & df_result ! ( maybe_plan) ?) ?
685+ } ;
686+
687+ Ok ( plan)
688+ }
580689}
581690
582691#[ cfg( test) ]
583692mod tests {
584- use arrow:: datatypes:: Schema ;
693+ use std:: sync:: Mutex ;
694+
695+ use arrow:: datatypes:: { DataType , Field , Schema } ;
696+ use datafusion:: common:: { Column , DFSchema } ;
697+ use datafusion:: logical_expr:: dml:: { MergeIntoAction , MergeIntoClauseKind } ;
698+ use datafusion:: physical_plan:: empty:: EmptyExec ;
585699 use datafusion:: prelude:: { SessionContext , col, lit} ;
586700 use datafusion_execution:: TaskContextProvider ;
587701
588702 use super :: * ;
589703
704+ type CapturedMerge = ( usize , usize , String , Vec < String > ) ;
705+
590706 fn create_test_table_provider ( ) -> Result < Arc < dyn TableProvider > > {
591707 use arrow:: datatypes:: Field ;
592708 use datafusion:: arrow:: array:: Float32Array ;
@@ -613,6 +729,122 @@ mod tests {
613729 ) ?) )
614730 }
615731
732+ #[ derive( Debug ) ]
733+ struct CaptureMergeProvider {
734+ schema : SchemaRef ,
735+ captured : Mutex < Option < CapturedMerge > > ,
736+ }
737+
738+ #[ async_trait]
739+ impl TableProvider for CaptureMergeProvider {
740+ fn schema ( & self ) -> SchemaRef {
741+ Arc :: clone ( & self . schema )
742+ }
743+
744+ fn table_type ( & self ) -> TableType {
745+ TableType :: Base
746+ }
747+
748+ async fn scan (
749+ & self ,
750+ _state : & dyn Session ,
751+ _projection : Option < & Vec < usize > > ,
752+ _filters : & [ Expr ] ,
753+ _limit : Option < usize > ,
754+ ) -> Result < Arc < dyn ExecutionPlan > > {
755+ Ok ( Arc :: new ( EmptyExec :: new ( Arc :: clone ( & self . schema ) ) ) )
756+ }
757+
758+ async fn merge_into (
759+ & self ,
760+ state : & dyn Session ,
761+ source : Arc < dyn ExecutionPlan > ,
762+ merge_schema : DFSchemaRef ,
763+ on : Expr ,
764+ clauses : Vec < MergeIntoClause > ,
765+ ) -> Result < Arc < dyn ExecutionPlan > > {
766+ let target_id =
767+ merge_schema. index_of_column ( & Column :: new ( Some ( "target" ) , "id" ) ) ?;
768+ let source_id =
769+ merge_schema. index_of_column ( & Column :: new ( Some ( "source" ) , "id" ) ) ?;
770+ let physical_on = state. create_physical_expr ( on, & merge_schema) ?;
771+ let clauses = clauses. iter ( ) . map ( |clause| format ! ( "{clause:?}" ) ) . collect ( ) ;
772+ * self . captured . lock ( ) . unwrap ( ) =
773+ Some ( ( target_id, source_id, format ! ( "{physical_on:?}" ) , clauses) ) ;
774+ Ok ( source)
775+ }
776+ }
777+
778+ #[ tokio:: test]
779+ async fn test_round_trip_ffi_table_provider_merge_into ( ) -> Result < ( ) > {
780+ let schema = Arc :: new ( Schema :: new ( vec ! [
781+ Field :: new( "id" , DataType :: Int32 , false ) ,
782+ Field :: new( "val" , DataType :: Int64 , true ) ,
783+ ] ) ) ;
784+ let provider = Arc :: new ( CaptureMergeProvider {
785+ schema : Arc :: clone ( & schema) ,
786+ captured : Mutex :: new ( None ) ,
787+ } ) ;
788+ let ctx = Arc :: new ( SessionContext :: new ( ) ) ;
789+ let task_ctx_provider = Arc :: clone ( & ctx) as Arc < dyn TaskContextProvider > ;
790+ let task_ctx_provider = FFI_TaskContextProvider :: from ( & task_ctx_provider) ;
791+
792+ let mut ffi_provider = FFI_TableProvider :: new (
793+ Arc :: clone ( & provider) as Arc < dyn TableProvider > ,
794+ true ,
795+ None ,
796+ task_ctx_provider,
797+ None ,
798+ ) ;
799+ ffi_provider. library_marker_id = crate :: mock_foreign_marker_id;
800+ let foreign: Arc < dyn TableProvider > = ( & ffi_provider) . into ( ) ;
801+
802+ let target_schema = DFSchema :: try_from_qualified_schema ( "target" , & schema) ?;
803+ let source_schema = DFSchema :: try_from_qualified_schema ( "source" , & schema) ?;
804+ let merge_schema = Arc :: new ( target_schema. join ( & source_schema) ?) ;
805+ let source: Arc < dyn ExecutionPlan > =
806+ Arc :: new ( EmptyExec :: new ( Arc :: clone ( & schema) ) ) ;
807+ let clauses = vec ! [
808+ MergeIntoClause {
809+ kind: MergeIntoClauseKind :: Matched ,
810+ predicate: Some ( col( "target.val" ) . is_null( ) ) ,
811+ action: MergeIntoAction :: Update ( vec![ (
812+ "val" . to_string( ) ,
813+ col( "source.val" ) ,
814+ ) ] ) ,
815+ } ,
816+ MergeIntoClause {
817+ kind: MergeIntoClauseKind :: NotMatched ,
818+ predicate: None ,
819+ action: MergeIntoAction :: Insert {
820+ columns: vec![ "id" . to_string( ) , "val" . to_string( ) ] ,
821+ values: vec![ col( "source.id" ) , col( "source.val" ) ] ,
822+ } ,
823+ } ,
824+ ] ;
825+
826+ foreign
827+ . merge_into (
828+ & ctx. state ( ) ,
829+ source,
830+ merge_schema,
831+ col ( "target.id" ) . eq ( col ( "source.id" ) ) ,
832+ clauses,
833+ )
834+ . await ?;
835+
836+ let captured = provider. captured . lock ( ) . unwrap ( ) ;
837+ let ( target_id, source_id, physical_on, clauses) =
838+ captured. as_ref ( ) . expect ( "merge_into should be called" ) ;
839+ assert_eq ! ( ( * target_id, * source_id) , ( 0 , 2 ) ) ;
840+ assert ! ( physical_on. contains( "index: 0" ) ) ;
841+ assert ! ( physical_on. contains( "index: 2" ) ) ;
842+ assert_eq ! ( clauses. len( ) , 2 ) ;
843+ assert ! ( clauses[ 0 ] . contains( "source" ) ) ;
844+ assert ! ( clauses[ 1 ] . contains( "Insert" ) ) ;
845+ Ok ( ( ) )
846+ }
847+
616848 #[ tokio:: test]
617849 async fn test_round_trip_ffi_table_provider_scan ( ) -> Result < ( ) > {
618850 let provider = create_test_table_provider ( ) ?;
0 commit comments