From 54f894ef8f8b4e4f03092d08c7d50a5da07dc673 Mon Sep 17 00:00:00 2001 From: Yifan Chen <30335308+emecii@users.noreply.github.com> Date: Wed, 9 Sep 2026 05:31:27 -0700 Subject: [PATCH] fix: preserve scalar UDF strictness across FFI Forward ScalarUDFImpl::is_strict through FFI_ScalarUDF and cover both forced-foreign and cross-library round trips. Generated-by: Codex (GPT-5) --- datafusion/ffi/src/tests/udf_udaf_udwf.rs | 4 ++++ datafusion/ffi/src/udf/mod.rs | 17 +++++++++++++++++ datafusion/ffi/tests/ffi_udf.rs | 2 ++ 3 files changed, 23 insertions(+) diff --git a/datafusion/ffi/src/tests/udf_udaf_udwf.rs b/datafusion/ffi/src/tests/udf_udaf_udwf.rs index 830c639c743d6..c5ca9056bce4e 100644 --- a/datafusion/ffi/src/tests/udf_udaf_udwf.rs +++ b/datafusion/ffi/src/tests/udf_udaf_udwf.rs @@ -150,6 +150,10 @@ impl ScalarUDFImpl for PlacementUDF { datafusion_common::internal_err!("placement_udf is not meant to be invoked") } + fn is_strict(&self) -> bool { + true + } + fn placement(&self, args: &[ExpressionPlacement]) -> ExpressionPlacement { // Push to the leaves only for a (Column, Literal) pairing, so the // test catches dropped, reordered, or truncated arguments. diff --git a/datafusion/ffi/src/udf/mod.rs b/datafusion/ffi/src/udf/mod.rs index d14614f1474a3..1d70b678645b8 100644 --- a/datafusion/ffi/src/udf/mod.rs +++ b/datafusion/ffi/src/udf/mod.rs @@ -130,6 +130,9 @@ pub struct FFI_ScalarUDF { udf: &Self, config: FFI_ConfigOptions, ) -> FFI_Result>, + + /// FFI equivalent to [`ScalarUDFImpl::is_strict`]. + pub is_strict: unsafe extern "C" fn(udf: &Self) -> bool, } unsafe impl Send for FFI_ScalarUDF {} @@ -193,6 +196,10 @@ unsafe extern "C" fn placement_fn_wrapper( udf.inner().placement(&args).into() } +unsafe extern "C" fn is_strict_fn_wrapper(udf: &FFI_ScalarUDF) -> bool { + udf.inner().is_strict() +} + unsafe extern "C" fn preserves_lex_ordering_fn_wrapper( udf: &FFI_ScalarUDF, inputs: SVec, @@ -321,6 +328,7 @@ impl From> for FFI_ScalarUDF { library_marker_id: crate::get_library_marker_id, preserves_lex_ordering: preserves_lex_ordering_fn_wrapper, with_updated_config: with_updated_config_fn_wrapper, + is_strict: is_strict_fn_wrapper, } } } @@ -502,6 +510,10 @@ impl ScalarUDFImpl for ForeignScalarUDF { self.udf.short_circuits } + fn is_strict(&self) -> bool { + unsafe { (self.udf.is_strict)(&self.udf) } + } + fn coerce_types(&self, arg_types: &[DataType]) -> Result> { unsafe { let arg_types = vec_datatype_to_rvec_wrapped(arg_types)?; @@ -576,6 +588,10 @@ mod tests { internal_err!("placement_udf is not meant to be invoked") } + fn is_strict(&self) -> bool { + true + } + fn placement(&self, args: &[ExpressionPlacement]) -> ExpressionPlacement { // Push to the leaves only for a (Column, Literal) pairing, so the // test catches dropped, reordered, or truncated arguments. @@ -663,6 +679,7 @@ mod tests { ffi_udf.library_marker_id = crate::mock_foreign_marker_id; let foreign_udf: Arc = (&ffi_udf).into(); assert!(foreign_udf.is::()); + assert!(foreign_udf.is_strict()); // Without the plumbing the override is dropped and every call is // KeepInPlace. The three cases also check the arguments survive the diff --git a/datafusion/ffi/tests/ffi_udf.rs b/datafusion/ffi/tests/ffi_udf.rs index 73fafef13415b..3ecf7043bf91a 100644 --- a/datafusion/ffi/tests/ffi_udf.rs +++ b/datafusion/ffi/tests/ffi_udf.rs @@ -100,6 +100,8 @@ mod tests { let ffi_placement_func = (module.create_placement_udf)(); let foreign_func: Arc = (&ffi_placement_func).into(); + assert!(foreign_func.is_strict()); + // The override pushes to the leaves only for (Column, Literal), so these // also check the arguments cross the boundary in order. assert_eq!(