@@ -164,20 +164,20 @@ impl FederationPlanner for SQLFederationPlanner {
164164pub struct VirtualExecutionPlan {
165165 plan : LogicalPlan ,
166166 executor : Arc < dyn SQLExecutor > ,
167- props : PlanProperties ,
167+ props : Arc < PlanProperties > ,
168168 statistics : Statistics ,
169169 filters : Vec < Arc < dyn PhysicalExpr > > ,
170170}
171171
172172impl VirtualExecutionPlan {
173173 pub fn new ( plan : LogicalPlan , executor : Arc < dyn SQLExecutor > , statistics : Statistics ) -> Self {
174174 let schema: Schema = plan. schema ( ) . as_arrow ( ) . clone ( ) ;
175- let props = PlanProperties :: new (
175+ let props = Arc :: new ( PlanProperties :: new (
176176 EquivalenceProperties :: new ( Arc :: new ( schema) ) ,
177177 Partitioning :: UnknownPartitioning ( 1 ) ,
178178 EmissionType :: Incremental ,
179179 Boundedness :: Bounded ,
180- ) ;
180+ ) ) ;
181181 Self {
182182 plan,
183183 executor,
@@ -401,7 +401,7 @@ impl ExecutionPlan for VirtualExecutionPlan {
401401 . execute ( & self . final_sql ( ) ?, self . schema ( ) , & self . filters )
402402 }
403403
404- fn properties ( & self ) -> & PlanProperties {
404+ fn properties ( & self ) -> & Arc < PlanProperties > {
405405 & self . props
406406 }
407407
@@ -766,6 +766,57 @@ mod tests {
766766 Ok ( ( ) )
767767 }
768768
769+ /// EXPLAIN ANALYZE must not federate the Analyze wrapper — only the inner
770+ /// query should be federated. Otherwise the SQL Unparser fails because it
771+ /// cannot convert Analyze to SQL.
772+ #[ tokio:: test]
773+ async fn explain_analyze_not_federated ( ) -> Result < ( ) , DataFusionError > {
774+ let executor = TestExecutor {
775+ compute_context : "a" . into ( ) ,
776+ } ;
777+
778+ let table_ref = "test_table" . to_string ( ) ;
779+ let table = get_test_table_provider ( table_ref. clone ( ) , executor) ;
780+
781+ let state = crate :: default_session_state ( ) ;
782+ let ctx = SessionContext :: new_with_state ( state) ;
783+ ctx. register_table ( table_ref, table) . unwrap ( ) ;
784+
785+ let plan = ctx
786+ . sql ( "EXPLAIN ANALYZE SELECT * FROM test_table" )
787+ . await ?
788+ . into_optimized_plan ( ) ?;
789+
790+ // The top-level node must be Analyze, not Federated.
791+ assert ! (
792+ matches!( plan, LogicalPlan :: Analyze ( _) ) ,
793+ "Expected Analyze at root, got: {}" ,
794+ plan. display_indent( )
795+ ) ;
796+
797+ // The inner plan should contain a Federated extension node.
798+ let mut found_federated = false ;
799+ plan. apply ( |node| {
800+ if let LogicalPlan :: Extension ( ext) = node {
801+ if ext. node . name ( ) == "Federated" {
802+ found_federated = true ;
803+ return Ok ( TreeNodeRecursion :: Stop ) ;
804+ }
805+ }
806+ Ok ( TreeNodeRecursion :: Continue )
807+ } ) ?;
808+ assert ! (
809+ found_federated,
810+ "Expected a Federated node inside the Analyze plan"
811+ ) ;
812+
813+ // Physical planning should succeed (this is where it used to fail).
814+ let physical_plan = ctx. state ( ) . create_physical_plan ( & plan) . await ?;
815+ assert_eq ! ( physical_plan. name( ) , "AnalyzeExec" ) ;
816+
817+ Ok ( ( ) )
818+ }
819+
769820 #[ tokio:: test]
770821 async fn sql_query_rewriter_hook_invoked_and_rewrites_sql ( ) -> Result < ( ) , DataFusionError > {
771822 let executor = TestExecutor {
0 commit comments