@@ -219,3 +219,153 @@ impl PhysicalOptimizerRule for ForeignPhysicalOptimizerRule {
219219 unsafe { ( self . 0 . schema_check ) ( & self . 0 ) }
220220 }
221221}
222+
223+ #[ cfg( test) ]
224+ mod tests {
225+ use std:: sync:: Arc ;
226+
227+ use arrow:: datatypes:: { DataType , Field , Schema } ;
228+ use datafusion_common:: config:: ConfigOptions ;
229+ use datafusion_common:: error:: Result ;
230+ use datafusion_physical_optimizer:: PhysicalOptimizerRule ;
231+ use datafusion_physical_plan:: ExecutionPlan ;
232+
233+ use crate :: execution_plan:: tests:: EmptyExec ;
234+
235+ use super :: * ;
236+
237+ #[ derive( Debug ) ]
238+ struct NoOpRule {
239+ schema_check : bool ,
240+ }
241+
242+ impl PhysicalOptimizerRule for NoOpRule {
243+ fn optimize (
244+ & self ,
245+ plan : Arc < dyn ExecutionPlan > ,
246+ _config : & ConfigOptions ,
247+ ) -> Result < Arc < dyn ExecutionPlan > > {
248+ Ok ( plan)
249+ }
250+
251+ fn name ( & self ) -> & str {
252+ "no_op_rule"
253+ }
254+
255+ fn schema_check ( & self ) -> bool {
256+ self . schema_check
257+ }
258+ }
259+
260+ fn create_test_plan ( ) -> Arc < dyn ExecutionPlan > {
261+ let schema =
262+ Arc :: new ( Schema :: new ( vec ! [ Field :: new( "a" , DataType :: Float32 , false ) ] ) ) ;
263+ Arc :: new ( EmptyExec :: new ( schema) )
264+ }
265+
266+ #[ test]
267+ fn test_round_trip_ffi_physical_optimizer_rule ( ) -> Result < ( ) > {
268+ for expected_schema_check in [ true , false ] {
269+ let rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > = Arc :: new ( NoOpRule {
270+ schema_check : expected_schema_check,
271+ } ) ;
272+
273+ let mut ffi_rule = FFI_PhysicalOptimizerRule :: new ( rule, None ) ;
274+ ffi_rule. library_marker_id = crate :: mock_foreign_marker_id;
275+
276+ let foreign_rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
277+ ( & ffi_rule) . into ( ) ;
278+
279+ assert_eq ! ( foreign_rule. name( ) , "no_op_rule" ) ;
280+ assert_eq ! ( foreign_rule. schema_check( ) , expected_schema_check) ;
281+ }
282+
283+ Ok ( ( ) )
284+ }
285+
286+ #[ test]
287+ fn test_round_trip_optimize ( ) -> Result < ( ) > {
288+ let rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
289+ Arc :: new ( NoOpRule { schema_check : true } ) ;
290+
291+ let mut ffi_rule = FFI_PhysicalOptimizerRule :: new ( rule, None ) ;
292+ ffi_rule. library_marker_id = crate :: mock_foreign_marker_id;
293+
294+ let foreign_rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
295+ ( & ffi_rule) . into ( ) ;
296+
297+ let plan = create_test_plan ( ) ;
298+ let config = ConfigOptions :: new ( ) ;
299+
300+ let optimized = foreign_rule. optimize ( plan, & config) ?;
301+ assert_eq ! ( optimized. name( ) , "empty-exec" ) ;
302+
303+ Ok ( ( ) )
304+ }
305+
306+ #[ test]
307+ fn test_local_bypass ( ) -> Result < ( ) > {
308+ let rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
309+ Arc :: new ( NoOpRule { schema_check : true } ) ;
310+
311+ // Without mock marker, local bypass should return the original rule
312+ let ffi_rule = FFI_PhysicalOptimizerRule :: new ( rule, None ) ;
313+ let recovered: Arc < dyn PhysicalOptimizerRule + Send + Sync > = ( & ffi_rule) . into ( ) ;
314+ let any_ref: & dyn std:: any:: Any = & * recovered;
315+ assert ! ( any_ref. downcast_ref:: <NoOpRule >( ) . is_some( ) ) ;
316+
317+ // With mock marker, should wrap in ForeignPhysicalOptimizerRule
318+ let rule2: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
319+ Arc :: new ( NoOpRule { schema_check : true } ) ;
320+ let mut ffi_rule2 = FFI_PhysicalOptimizerRule :: new ( rule2, None ) ;
321+ ffi_rule2. library_marker_id = crate :: mock_foreign_marker_id;
322+ let recovered2: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
323+ ( & ffi_rule2) . into ( ) ;
324+ let any_ref2: & dyn std:: any:: Any = & * recovered2;
325+ assert ! (
326+ any_ref2
327+ . downcast_ref:: <ForeignPhysicalOptimizerRule >( )
328+ . is_some( )
329+ ) ;
330+
331+ Ok ( ( ) )
332+ }
333+
334+ #[ test]
335+ fn test_clone ( ) -> Result < ( ) > {
336+ let rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
337+ Arc :: new ( NoOpRule { schema_check : true } ) ;
338+
339+ let ffi_rule = FFI_PhysicalOptimizerRule :: new ( rule, None ) ;
340+ let cloned = ffi_rule. clone ( ) ;
341+
342+ assert_eq ! ( unsafe { ( ffi_rule. name) ( & ffi_rule) . as_str( ) } , unsafe {
343+ ( cloned. name) ( & cloned) . as_str( )
344+ } ) ;
345+
346+ Ok ( ( ) )
347+ }
348+
349+ #[ test]
350+ fn test_foreign_rule_rewrap_bypass ( ) -> Result < ( ) > {
351+ // When creating an FFI wrapper from a ForeignPhysicalOptimizerRule,
352+ // it should return the inner FFI rule rather than double-wrapping.
353+ let rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
354+ Arc :: new ( NoOpRule { schema_check : true } ) ;
355+
356+ let mut ffi_rule = FFI_PhysicalOptimizerRule :: new ( rule, None ) ;
357+ ffi_rule. library_marker_id = crate :: mock_foreign_marker_id;
358+
359+ let foreign_rule: Arc < dyn PhysicalOptimizerRule + Send + Sync > =
360+ ( & ffi_rule) . into ( ) ;
361+
362+ // Now wrap the foreign rule back into FFI - should not double-wrap
363+ let re_wrapped = FFI_PhysicalOptimizerRule :: new ( foreign_rule, None ) ;
364+ assert_eq ! (
365+ unsafe { ( re_wrapped. name) ( & re_wrapped) . as_str( ) } ,
366+ "no_op_rule"
367+ ) ;
368+
369+ Ok ( ( ) )
370+ }
371+ }
0 commit comments