1use std::sync::{Arc, Mutex};
4
5use arrow_array::RecordBatch;
6use arrow_schema::SchemaRef;
7use datafusion::catalog::streaming::StreamingTable;
8use datafusion::datasource::MemTable;
9use datafusion::physical_plan::SendableRecordBatchStream;
10use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
11use datafusion::physical_plan::streaming::PartitionStream;
12use datafusion::prelude::*;
13use futures::StreamExt;
14
15use crate::{ReadStatError, ReadStatWriter, WriteConfig};
16
17pub type RecordBatchReceiver = crossbeam::channel::Receiver<Result<RecordBatch, ReadStatError>>;
19pub type RecordBatchSender = crossbeam::channel::Sender<Result<RecordBatch, ReadStatError>>;
21pub type AsyncRecordBatchReceiver = tokio::sync::mpsc::Receiver<Result<RecordBatch, ReadStatError>>;
23pub type AsyncRecordBatchSender = tokio::sync::mpsc::Sender<Result<RecordBatch, ReadStatError>>;
25
26#[must_use]
31pub fn record_batch_channel(capacity: usize) -> (RecordBatchSender, RecordBatchReceiver) {
32 crossbeam::channel::bounded(capacity)
33}
34
35pub fn async_record_batch_channel(
41 capacity: usize,
42) -> Result<(AsyncRecordBatchSender, AsyncRecordBatchReceiver), ReadStatError> {
43 if capacity == 0 {
44 return Err(ReadStatError::Other(
45 "async record batch channel capacity must be greater than zero".into(),
46 ));
47 }
48 Ok(tokio::sync::mpsc::channel(capacity))
49}
50
51fn runtime() -> Result<tokio::runtime::Runtime, ReadStatError> {
52 if tokio::runtime::Handle::try_current().is_ok() {
53 return Err(ReadStatError::SyncSqlInAsyncRuntime);
54 }
55 Ok(tokio::runtime::Runtime::new()?)
56}
57
58pub fn execute_sql(
60 batches: Vec<RecordBatch>,
61 schema: SchemaRef,
62 table_name: &str,
63 sql: &str,
64) -> Result<Vec<RecordBatch>, ReadStatError> {
65 runtime()?.block_on(execute_sql_async(batches, schema, table_name, sql))
66}
67
68pub async fn execute_sql_async(
70 batches: Vec<RecordBatch>,
71 schema: SchemaRef,
72 table_name: &str,
73 sql: &str,
74) -> Result<Vec<RecordBatch>, ReadStatError> {
75 let ctx = SessionContext::new();
76 ctx.register_table(
77 table_name,
78 Arc::new(MemTable::try_new(schema, vec![batches])?),
79 )?;
80 collect_with_empty_batch(ctx.sql(sql).await?).await
81}
82
83async fn collect_with_empty_batch(df: DataFrame) -> Result<Vec<RecordBatch>, ReadStatError> {
84 let schema = Arc::new(df.schema().as_arrow().clone());
85 let results = df.collect().await?;
86 Ok(if results.is_empty() {
87 vec![RecordBatch::new_empty(schema)]
88 } else {
89 results
90 })
91}
92
93#[derive(Debug)]
96struct ChannelPartitionStream {
97 schema: SchemaRef,
98 receiver: Arc<Mutex<Option<InputReceiver>>>,
99}
100
101impl ChannelPartitionStream {
102 fn new(schema: SchemaRef, receiver: InputReceiver) -> Self {
103 Self {
104 schema,
105 receiver: Arc::new(Mutex::new(Some(receiver))),
106 }
107 }
108}
109
110#[derive(Debug)]
111enum InputReceiver {
112 Blocking(RecordBatchReceiver),
113 Async(AsyncRecordBatchReceiver),
114}
115
116impl PartitionStream for ChannelPartitionStream {
117 fn schema(&self) -> &SchemaRef {
118 &self.schema
119 }
120
121 fn execute(&self, _ctx: Arc<datafusion::execution::TaskContext>) -> SendableRecordBatchStream {
122 let receiver = self
123 .receiver
124 .lock()
125 .unwrap_or_else(|e| e.into_inner())
126 .take();
127 let schema = self.schema.clone();
128 let stream = match receiver {
129 Some(receiver) => {
130 let receiver = match receiver {
131 InputReceiver::Async(receiver) => receiver,
132 InputReceiver::Blocking(receiver) => {
133 let (sender, receiver_async) = tokio::sync::mpsc::channel(2);
134 std::thread::spawn(move || {
137 loop {
138 if sender.is_closed() {
139 break;
140 }
141 match receiver
142 .recv_timeout(std::time::Duration::from_millis(100))
143 {
144 Ok(result) => {
145 if sender.blocking_send(result).is_err() {
146 break;
147 }
148 }
149 Err(crossbeam::channel::RecvTimeoutError::Timeout) => {}
150 Err(crossbeam::channel::RecvTimeoutError::Disconnected) => break,
151 }
152 }
153 });
154 receiver_async
155 }
156 };
157 futures::stream::unfold(receiver, |mut receiver| async move {
158 receiver.recv().await.map(|batch| {
159 let batch = batch.map_err(|error| {
160 datafusion::error::DataFusionError::External(Box::new(error))
161 });
162 (batch, receiver)
163 })
164 })
165 .left_stream()
166 }
167 None => futures::stream::once(async {
168 Err(datafusion::error::DataFusionError::Execution(
169 "channel-backed StreamingTable can only be executed once; use execute_sql for plans that scan input multiple times".into(),
170 ))
171 })
172 .right_stream(),
173 };
174 Box::pin(RecordBatchStreamAdapter::new(schema, stream))
175 }
176}
177
178fn streaming_context(
179 receiver: InputReceiver,
180 schema: SchemaRef,
181 table_name: &str,
182) -> Result<SessionContext, ReadStatError> {
183 let ctx = SessionContext::new();
184 let partition = ChannelPartitionStream::new(schema.clone(), receiver);
185 ctx.register_table(
186 table_name,
187 Arc::new(StreamingTable::try_new(schema, vec![Arc::new(partition)])?),
188 )?;
189 Ok(ctx)
190}
191
192pub fn execute_sql_stream(
196 receiver: RecordBatchReceiver,
197 schema: SchemaRef,
198 table_name: &str,
199 sql: &str,
200) -> Result<Vec<RecordBatch>, ReadStatError> {
201 runtime()?.block_on(execute_sql_from_input_async(
202 InputReceiver::Blocking(receiver),
203 schema,
204 table_name,
205 sql,
206 ))
207}
208
209pub async fn execute_sql_stream_async(
215 receiver: AsyncRecordBatchReceiver,
216 schema: SchemaRef,
217 table_name: &str,
218 sql: &str,
219) -> Result<Vec<RecordBatch>, ReadStatError> {
220 execute_sql_from_input_async(InputReceiver::Async(receiver), schema, table_name, sql).await
221}
222
223async fn execute_sql_from_input_async(
224 receiver: InputReceiver,
225 schema: SchemaRef,
226 table_name: &str,
227 sql: &str,
228) -> Result<Vec<RecordBatch>, ReadStatError> {
229 let ctx = streaming_context(receiver, schema, table_name)?;
230 collect_with_empty_batch(ctx.sql(sql).await?).await
231}
232
233pub fn execute_sql_and_write_stream(
235 receiver: RecordBatchReceiver,
236 schema: SchemaRef,
237 table_name: &str,
238 sql: &str,
239 config: &WriteConfig,
240) -> Result<usize, ReadStatError> {
241 runtime()?.block_on(execute_sql_and_write_from_input_async(
242 InputReceiver::Blocking(receiver),
243 schema,
244 table_name,
245 sql,
246 config,
247 ))
248}
249
250pub async fn execute_sql_and_write_stream_async(
253 receiver: AsyncRecordBatchReceiver,
254 schema: SchemaRef,
255 table_name: &str,
256 sql: &str,
257 config: &WriteConfig,
258) -> Result<usize, ReadStatError> {
259 execute_sql_and_write_from_input_async(
260 InputReceiver::Async(receiver),
261 schema,
262 table_name,
263 sql,
264 config,
265 )
266 .await
267}
268
269async fn execute_sql_and_write_from_input_async(
270 receiver: InputReceiver,
271 schema: SchemaRef,
272 table_name: &str,
273 sql: &str,
274 config: &WriteConfig,
275) -> Result<usize, ReadStatError> {
276 let ctx = streaming_context(receiver, schema, table_name)?;
277 let df = ctx.sql(sql).await?;
278 let result_schema = Arc::new(df.schema().as_arrow().clone());
279 let mut stream = df.execute_stream().await?;
280 enum Message {
281 Batch(RecordBatch),
282 Finish,
283 }
284 let (sender, mut receiver) = tokio::sync::mpsc::channel(2);
285 let config = config.clone();
286 let writer_task = tokio::task::spawn_blocking(move || {
287 let mut writer = ReadStatWriter::new(config, result_schema)?;
288 while let Some(message) = receiver.blocking_recv() {
289 match message {
290 Message::Batch(batch) => writer.write(&batch)?,
291 Message::Finish => return writer.finish(),
292 }
293 }
294 Err(ReadStatError::Other(
295 "SQL output was cancelled before the writer finished".into(),
296 ))
297 });
298 while let Some(batch) = stream.next().await {
299 sender
300 .send(Message::Batch(batch?))
301 .await
302 .map_err(|_| ReadStatError::Other("SQL writer stopped unexpectedly".into()))?;
303 }
304 sender
305 .send(Message::Finish)
306 .await
307 .map_err(|_| ReadStatError::Other("SQL writer stopped unexpectedly".into()))?;
308 drop(sender);
309 writer_task
310 .await
311 .map_err(|error| ReadStatError::Other(format!("SQL writer task failed: {error}")))?
312}
313
314pub fn read_sql_file(path: &std::path::Path) -> Result<String, ReadStatError> {
316 let sql = std::fs::read_to_string(path)?.trim().to_string();
317 if sql.is_empty() {
318 return Err(ReadStatError::EmptySqlFile(path.to_path_buf()));
319 }
320 Ok(sql)
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use arrow_array::{Int32Array, RecordBatch};
327 use arrow_schema::{DataType, Field, Schema};
328
329 fn input() -> (SchemaRef, RecordBatch) {
330 let schema = Arc::new(Schema::new(vec![Field::new(
331 "value",
332 DataType::Int32,
333 false,
334 )]));
335 let batch =
336 RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(vec![1, 2]))])
337 .unwrap();
338 (schema, batch)
339 }
340
341 #[tokio::test]
342 async fn async_query_and_sync_runtime_guard() {
343 let (schema, batch) = input();
344 let result = execute_sql_async(vec![batch.clone()], schema.clone(), "t", "select * from t")
345 .await
346 .unwrap();
347 assert_eq!(result.iter().map(RecordBatch::num_rows).sum::<usize>(), 2);
348 assert!(matches!(
349 execute_sql(vec![batch], schema, "t", "select * from t"),
350 Err(ReadStatError::SyncSqlInAsyncRuntime)
351 ));
352 }
353
354 #[tokio::test]
355 async fn streaming_propagates_channel_errors_and_preserves_empty_schema() {
356 let (schema, batch) = input();
357 let (sender, receiver) = async_record_batch_channel(1).unwrap();
358 sender
359 .send(Err(ReadStatError::Other("source failed".into())))
360 .await
361 .unwrap();
362 drop(sender);
363 let error = execute_sql_stream_async(receiver, schema.clone(), "t", "select * from t")
364 .await
365 .unwrap_err();
366 assert!(error.to_string().contains("source failed"));
367
368 let (sender, receiver) = async_record_batch_channel(1).unwrap();
369 sender.send(Ok(batch)).await.unwrap();
370 drop(sender);
371 let result = execute_sql_stream_async(receiver, schema, "t", "select * from t where 1=0")
372 .await
373 .unwrap();
374 assert_eq!(result.len(), 1);
375 assert_eq!(result[0].num_rows(), 0);
376 assert_eq!(result[0].num_columns(), 1);
377 }
378
379 #[tokio::test(flavor = "current_thread")]
380 async fn streaming_does_not_block_current_thread_runtime() {
381 let (schema, batch) = input();
382 let (sender, receiver) = async_record_batch_channel(1).unwrap();
383 tokio::spawn(async move {
384 tokio::task::yield_now().await;
385 sender.send(Ok(batch)).await.unwrap();
386 });
387 let result = execute_sql_stream_async(receiver, schema, "t", "select * from t")
388 .await
389 .unwrap();
390 assert_eq!(result.iter().map(RecordBatch::num_rows).sum::<usize>(), 2);
391 }
392}