Skip to content

Commit ba1d668

Browse files
committed
feat: Add MERGE INTO support to FFI TableProvider
1 parent 3a76cd3 commit ba1d668

4 files changed

Lines changed: 416 additions & 12 deletions

File tree

datafusion/ffi/src/table_provider.rs

Lines changed: 238 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ use arrow::datatypes::SchemaRef;
2222
use async_ffi::{FfiFuture, FutureExt};
2323
use async_trait::async_trait;
2424
use datafusion_catalog::{Session, TableProvider};
25-
use datafusion_common::Statistics;
2625
use datafusion_common::error::{DataFusionError, Result};
26+
use datafusion_common::{DFSchemaRef, Statistics};
2727
use datafusion_execution::TaskContext;
28-
use datafusion_expr::dml::InsertOp;
28+
use datafusion_expr::dml::{InsertOp, MergeIntoClause, MergeIntoOp};
2929
use datafusion_expr::{Expr, TableProviderFilterPushDown, TableType};
3030
use 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+
};
3335
use datafusion_proto::logical_plan::{
3436
DefaultLogicalExtensionCodec, LogicalExtensionCodec,
3537
};
36-
use datafusion_proto::protobuf::LogicalExprList;
38+
use datafusion_proto::protobuf::{DfSchema, LogicalExprList, MergeIntoOpNode};
3739
use prost::Message;
3840

3941
use 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+
339406
unsafe 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)]
583692
mod 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

Comments
 (0)