Skip to content

Commit 62125fd

Browse files
committed
Support for substrait plans
1 parent ae8d2bb commit 62125fd

8 files changed

Lines changed: 488 additions & 96 deletions

File tree

Cargo.lock

Lines changed: 253 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ datafusion = { git = "https://github.com/apache/datafusion", branch = "main", fe
4444
"avro",
4545
] }
4646
datafusion-proto = { git = "https://github.com/apache/datafusion", branch = "main" }
47+
datafusion-substrait = { git = "https://github.com/apache/datafusion", branch = "main" }
4748
env_logger = "0.11"
4849
futures = "0.3"
4950
itertools = "0.14"
@@ -53,6 +54,9 @@ log = "0.4"
5354
rand = "0.8"
5455
uuid = { version = "1.6", features = ["v4"] }
5556

57+
serde = { version = "1.0", features = ["derive"] }
58+
serde_json = "1.0"
59+
5660
object_store = { version = "0.12.0", features = [
5761
"aws",
5862
"gcp",

src/explain.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,10 @@ impl ExecutionPlan for DistributedExplainExec {
160160
self.distributed_stages.as_str(),
161161
]);
162162

163-
let batch =
164-
RecordBatch::try_new(schema.clone(), vec![Arc::new(plan_types), Arc::new(plans)])
165-
.map_err(|e| datafusion::error::DataFusionError::ArrowError(Box::new(e), None))?;
163+
let batch = RecordBatch::try_new(
164+
schema.clone(),
165+
vec![Arc::new(plan_types), Arc::new(plans)],
166+
).map_err(|e| datafusion::error::DataFusionError::ArrowError(Box::new(e), None))?;
166167

167168
// Use MemoryStream which is designed for DataFusion execution plans
168169
let stream = MemoryStream::try_new(vec![batch], schema, None)?;

src/flight.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ pub trait FlightSqlHandler: Send + Sync {
134134
request: Request<FlightDescriptor>,
135135
) -> Result<Response<FlightInfo>, Status>;
136136

137+
async fn get_flight_info_substrait(
138+
&self,
139+
query: arrow_flight::sql::CommandStatementSubstraitPlan,
140+
request: Request<FlightDescriptor>,
141+
) -> Result<Response<FlightInfo>, Status>;
142+
137143
async fn do_get_statement(
138144
&self,
139145
ticket: arrow_flight::sql::TicketStatementQuery,
@@ -163,6 +169,20 @@ impl FlightSqlService for FlightSqlServ {
163169
error!("Error in do_flight_info_statement: {:?}", e);
164170
})
165171
}
172+
173+
async fn get_flight_info_substrait_plan(
174+
&self,
175+
query: arrow_flight::sql::CommandStatementSubstraitPlan,
176+
request: Request<FlightDescriptor>,
177+
) -> Result<Response<FlightInfo>, Status> {
178+
self.handler
179+
.get_flight_info_substrait(query, request)
180+
.await
181+
.inspect_err(|e| {
182+
error!("Error in get_flight_info_substrait: {:?}", e);
183+
})
184+
}
185+
166186
async fn do_get_statement(
167187
&self,
168188
ticket: arrow_flight::sql::TicketStatementQuery,

src/flight_handlers.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,27 @@ impl FlightRequestHandler {
136136
Ok(Response::new(flight_info))
137137
}
138138

139+
pub async fn handle_substrait_info_request(&self, substrait_plan: datafusion_substrait::substrait::proto::Plan) -> Result<Response<FlightInfo>, Status> {
140+
let query_plan = self
141+
.planner
142+
.prepare_substrait_query(substrait_plan)
143+
.await
144+
.map_err(|e| Status::internal(format!("Could not prepare query {e:?}")))?;
145+
146+
debug!("get flight info: query id {}", query_plan.query_id);
147+
148+
let flight_info = self.create_flight_info_response(
149+
query_plan.query_id,
150+
query_plan.worker_addresses,
151+
query_plan.final_stage_id,
152+
query_plan.schema,
153+
None // Regular queries don't have explain data
154+
)?;
155+
156+
trace!("get_flight_info_statement done");
157+
Ok(Response::new(flight_info))
158+
}
159+
139160
/// Handle execution of EXPLAIN statement queries.
140161
///
141162
/// This function does not execute the plan but returns all plans we want to display to the user.

src/proxy_service.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ use std::sync::Arc;
1919

2020
use anyhow::{anyhow, Context};
2121
use arrow_flight::{
22-
flight_service_server::FlightServiceServer, FlightDescriptor, FlightInfo, Ticket,
22+
flight_service_server::FlightServiceServer, sql::TicketStatementQuery, FlightDescriptor, FlightInfo, Ticket
2323
};
24+
use datafusion_substrait::substrait;
2425
use parking_lot::Mutex;
2526
use prost::Message;
2627
use tokio::{
@@ -74,7 +75,7 @@ impl FlightSqlHandler for DfRayProxyHandler {
7475

7576
async fn do_get_statement(
7677
&self,
77-
ticket: arrow_flight::sql::TicketStatementQuery,
78+
ticket: TicketStatementQuery,
7879
request: Request<Ticket>,
7980
) -> Result<Response<crate::flight::DoGetStream>, Status> {
8081
trace!("do_get_statement");
@@ -98,6 +99,22 @@ impl FlightSqlHandler for DfRayProxyHandler {
9899
.await
99100
}
100101
}
102+
103+
async fn get_flight_info_substrait(
104+
&self,
105+
substrait: arrow_flight::sql::CommandStatementSubstraitPlan,
106+
_request: Request<FlightDescriptor>,
107+
) -> Result<Response<FlightInfo>, Status> {
108+
let plan = match &substrait.plan {
109+
Some(substrait_plan) => {
110+
substrait::proto::Plan::decode(substrait_plan.plan.as_ref())
111+
.map_err(|e| Status::invalid_argument(format!("Invalid Substrait plan: {e}")))?
112+
}
113+
None => return Err(Status::invalid_argument("Missing Substrait plan")),
114+
};
115+
116+
self.flight_handler.handle_substrait_info_request(plan).await
117+
}
101118
}
102119

103120
/// DFRayProcessorService is a Arrow Flight service that serves streams of
@@ -213,8 +230,7 @@ mod tests {
213230
use super::*;
214231
// Test-specific imports
215232
use arrow_flight::{
216-
sql::{CommandStatementQuery, TicketStatementQuery},
217-
FlightDescriptor, Ticket,
233+
sql::{CommandStatementQuery, TicketStatementQuery}, FlightDescriptor, Ticket
218234
};
219235
use prost::Message;
220236
use tonic::Request;
@@ -345,4 +361,5 @@ mod tests {
345361
// TODO: Add tests for regular (non-explain) queries
346362
// We might need to create integration or end-to-end test infrastructure for this because
347363
// they need workers
364+
348365
}

src/query_planner.rs

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use arrow::datatypes::SchemaRef;
55
use datafusion::{
66
logical_expr::LogicalPlan, physical_plan::ExecutionPlan, prelude::SessionContext,
77
};
8+
use datafusion_substrait::logical_plan::{consumer::from_substrait_plan};
89

910
use 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)]
207247
mod 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

Comments
 (0)