@@ -5,6 +5,7 @@ use arrow::datatypes::SchemaRef;
55use datafusion:: {
66 logical_expr:: LogicalPlan , physical_plan:: ExecutionPlan , prelude:: SessionContext ,
77} ;
8+ use datafusion_substrait:: logical_plan:: { consumer:: from_substrait_plan} ;
89
910use crate :: {
1011 explain:: { is_explain_query, DistributedExplainExec } ,
@@ -65,6 +66,38 @@ impl QueryPlanner {
6566 Self
6667 }
6768
69+ /// Dispatch a distributed query plan to the workers.
70+ async fn dispatch_query_plan ( & self , base_result : QueryPlanBase ) -> Result < QueryPlan > {
71+ if base_result. distributed_stages . is_empty ( ) {
72+ return Err ( anyhow ! ( "No stages generated for query" ) . into ( ) ) ;
73+ }
74+
75+ let worker_addrs = get_worker_addresses ( ) ?;
76+
77+ // The last stage produces the data returned to the client.
78+ let final_stage = & base_result. distributed_stages
79+ [ base_result. distributed_stages . len ( ) - 1 ] ;
80+ let schema = Arc :: clone ( & final_stage. plan . schema ( ) ) ;
81+ let final_stage_id = final_stage. stage_id ;
82+
83+ // Physically dispatch each stage to the worker pool, further dividing
84+ // them into partition groups.
85+ let final_workers = distribute_stages (
86+ & base_result. query_id ,
87+ base_result. distributed_stages ,
88+ worker_addrs,
89+ )
90+ . await ?;
91+
92+ Ok ( QueryPlan {
93+ query_id : base_result. query_id ,
94+ worker_addresses : final_workers,
95+ final_stage_id,
96+ schema,
97+ explain_data : None ,
98+ } )
99+ }
100+
68101 /// Common planning steps shared by both query and its EXPLAIN
69102 ///
70103 /// Prepare a query by parsing the SQL, planning it, and distributing the
@@ -78,9 +111,8 @@ impl QueryPlanner {
78111 let logical_plan = logical_planning ( sql, & ctx) . await ?;
79112 let physical_plan = physical_planning ( & logical_plan, & ctx) . await ?;
80113
81- // divide the physical plan into chunks (stages) that we can distribute to workers
82- let ( distributed_plan, distributed_stages) =
83- execution_planning ( physical_plan. clone ( ) , 8192 , Some ( 2 ) ) . await ?;
114+ // divide the physical plan into chunks (stages) that we can distribute to workers later in dispatch_query_plan
115+ let ( distributed_plan, distributed_stages) = execution_planning ( physical_plan. clone ( ) , 8192 , Some ( 2 ) ) . await ?;
84116
85117 Ok ( QueryPlanBase {
86118 query_id,
@@ -92,37 +124,45 @@ impl QueryPlanner {
92124 } )
93125 }
94126
95- /// Prepare a distributed query
127+ /// Prepare a distributed query (SQL entry point)
96128 pub async fn prepare_query ( & self , sql : & str ) -> Result < QueryPlan > {
97129 let base_result = self . prepare_query_base ( sql, "REGULAR" ) . await ?;
130+ self . dispatch_query_plan ( base_result) . await
131+ }
98132
99- if base_result. distributed_stages . is_empty ( ) {
100- return Err ( anyhow ! ( "No stages generated for query" ) . into ( ) ) ;
101- }
133+ /// Prepare a distributed query (Substrait entry point)
134+ pub async fn prepare_substrait_query ( & self , substrait_plan : datafusion_substrait:: substrait:: proto:: Plan ) -> Result < QueryPlan > {
135+ let base_result = self . prepare_substrait_query_base ( substrait_plan, "SUBSTRAIT" ) . await ?;
136+ self . dispatch_query_plan ( base_result) . await
137+ }
102138
103- let worker_addrs = get_worker_addresses ( ) ?;
139+ pub async fn prepare_substrait_query_base (
140+ & self ,
141+ substrait_plan : datafusion_substrait:: substrait:: proto:: Plan ,
142+ query_type : & str ,
143+ ) -> Result < QueryPlanBase > {
144+ debug ! ( "prepare_substrait_query_base: {} Substrait = {:#?}" , query_type, substrait_plan) ;
145+
146+ let query_id = uuid:: Uuid :: new_v4 ( ) . to_string ( ) ;
147+ let ctx = get_ctx ( ) . map_err ( |e| anyhow ! ( "Could not create context: {e}" ) ) ?;
104148
105- // gather some information we need to send back such that
106- // we can send a ticket to the client
107- let final_stage = & base_result. distributed_stages [ base_result. distributed_stages . len ( ) - 1 ] ;
108- let schema = Arc :: clone ( & final_stage. plan . schema ( ) ) ;
109- let final_stage_id = final_stage. stage_id ;
149+ let logical_plan = from_substrait_plan ( & ctx. state ( ) , & substrait_plan)
150+ . await
151+ . map_err ( |e| anyhow ! ( "Failed to convert DataFusion Logical Plan: {e}" ) ) ?;
110152
111- // distribute the stages to workers, further dividing them up
112- // into chunks of partitions (partition_groups)
113- let final_workers = distribute_stages (
114- & base_result. query_id ,
115- base_result. distributed_stages ,
116- worker_addrs,
117- )
118- . await ?;
119153
120- Ok ( QueryPlan {
121- query_id : base_result. query_id ,
122- worker_addresses : final_workers,
123- final_stage_id,
124- schema,
125- explain_data : None ,
154+ let physical_plan = physical_planning ( & logical_plan, & ctx) . await . map_err ( |e| anyhow ! ( "Failed to convert DataFusion Physical Plan: {e}" ) ) ?;
155+
156+ // divide the physical plan into chunks (stages) that we can distribute to workers later in dispatch_query_plan
157+ let ( distributed_plan, distributed_stages) = execution_planning ( physical_plan. clone ( ) , 8192 , Some ( 2 ) ) . await ?;
158+
159+ Ok ( QueryPlanBase {
160+ query_id,
161+ session_context : ctx,
162+ logical_plan,
163+ physical_plan,
164+ distributed_plan,
165+ distributed_stages,
126166 } )
127167 }
128168
@@ -206,17 +246,18 @@ impl QueryPlanner {
206246#[ cfg( test) ]
207247mod tests {
208248 use super :: * ;
249+ use std:: { fs:: { File } , path:: Path } ;
250+ use std:: io:: BufReader ;
251+
209252
210253 // //////////////////////////////////////////////////////////////
211254 // Test helper functions
212255 // //////////////////////////////////////////////////////////////
213256
214257 /// Set up mock worker environment for testing
215258 fn setup_mock_worker_env ( ) {
216- let mock_addrs = vec ! [
217- ( "mock_worker_1" . to_string( ) , "localhost:9001" . to_string( ) ) ,
218- ( "mock_worker_2" . to_string( ) , "localhost:9002" . to_string( ) ) ,
219- ] ;
259+ let mock_addrs = [ ( "mock_worker_1" . to_string ( ) , "localhost:9001" . to_string ( ) ) ,
260+ ( "mock_worker_2" . to_string ( ) , "localhost:9002" . to_string ( ) ) ] ;
220261 let mock_env_value = mock_addrs
221262 . iter ( )
222263 . map ( |( name, addr) | format ! ( "{}/{}" , name, addr) )
@@ -257,6 +298,34 @@ mod tests {
257298 }
258299 }
259300
301+ #[ tokio:: test]
302+ async fn test_prepare_substrait_query_base ( ) {
303+ let planner = QueryPlanner :: new ( ) ;
304+
305+ // Read the JSON plan and convert to binary Substrait protobuf bytes
306+ let plan = serde_json:: from_reader :: < _ , datafusion_substrait:: substrait:: proto:: Plan > ( BufReader :: new (
307+ File :: open ( Path :: new ( "testdata/substrait/select_one.substrait.json" ) ) . expect ( "file not found" ) ,
308+ ) ) . expect ( "failed to parse json" ) ;
309+
310+ let result = planner. prepare_substrait_query_base ( plan, "TEST" ) . await ;
311+
312+ if result. is_ok ( ) {
313+ let query_plan_base = result. unwrap ( ) ;
314+ // verify all fields have values
315+ assert ! ( !query_plan_base. query_id. is_empty( ) ) ;
316+ assert ! ( !query_plan_base. distributed_stages. is_empty( ) ) ;
317+ assert ! ( !query_plan_base. physical_plan. schema( ) . fields( ) . is_empty( ) ) ;
318+ // logical plan of select 1 on empty relation
319+ assert_eq ! ( query_plan_base. logical_plan. to_string( ) , "Projection: Int64(1) AS test_col\n Values: (Int64(0))" ) ;
320+ // physical plan of select 1 on empty releation is ProjectionExec
321+ assert_eq ! ( query_plan_base. physical_plan. name( ) , "ProjectionExec" ) ;
322+ } else {
323+ // If worker discovery fails, we expect a specific error
324+ let error_msg = format ! ( "{:?}" , result. unwrap_err( ) ) ;
325+ assert ! ( error_msg. contains( "worker" ) || error_msg. contains( "address" ) ) ;
326+ }
327+ }
328+
260329 #[ tokio:: test]
261330 async fn test_prepare_explain ( ) {
262331 let planner = QueryPlanner :: new ( ) ;
0 commit comments