Skip to main content

datafusion_catalog/
stream.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! TableProvider for stream sources, such as FIFO files
19
20use std::fmt::Formatter;
21use std::fs::{File, OpenOptions};
22use std::io::BufReader;
23use std::path::PathBuf;
24use std::str::FromStr;
25use std::sync::Arc;
26
27use crate::{Session, TableProvider, TableProviderFactory};
28use arrow::array::{RecordBatch, RecordBatchReader, RecordBatchWriter};
29use arrow::datatypes::SchemaRef;
30use datafusion_common::{Constraints, DataFusionError, Result, config_err, plan_err};
31use datafusion_common_runtime::SpawnedTask;
32use datafusion_datasource::sink::{DataSink, DataSinkExec};
33use datafusion_execution::{SendableRecordBatchStream, TaskContext};
34use datafusion_expr::dml::InsertOp;
35use datafusion_expr::{CreateExternalTable, Expr, SortExpr, TableType};
36use datafusion_physical_expr::create_lex_ordering;
37use datafusion_physical_plan::stream::RecordBatchReceiverStreamBuilder;
38use datafusion_physical_plan::streaming::{PartitionStream, StreamingTableExec};
39use datafusion_physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan};
40
41use async_trait::async_trait;
42use futures::StreamExt;
43
44/// A [`TableProviderFactory`] for [`StreamTable`]
45#[derive(Debug, Default)]
46pub struct StreamTableFactory {}
47
48#[async_trait]
49impl TableProviderFactory for StreamTableFactory {
50    async fn create(
51        &self,
52        state: &dyn Session,
53        cmd: &CreateExternalTable,
54    ) -> Result<Arc<dyn TableProvider>> {
55        let schema: SchemaRef = Arc::clone(cmd.schema.inner());
56        let location = match cmd.locations.as_slice() {
57            [single] => single.clone(),
58            _ => {
59                return config_err!(
60                    "Stream tables support exactly one location; \
61                     use a listing table to read multiple files"
62                );
63            }
64        };
65        let encoding = cmd.file_type.parse()?;
66        let header = if let Ok(opt) = cmd
67            .options
68            .get("format.has_header")
69            .map(|has_header| bool::from_str(has_header.to_lowercase().as_str()))
70            .transpose()
71        {
72            opt.unwrap_or(false)
73        } else {
74            return config_err!(
75                "Valid values for format.has_header option are 'true' or 'false'"
76            );
77        };
78
79        let source = FileStreamProvider::new_file(schema, location.into())
80            .with_encoding(encoding)
81            .with_batch_size(state.config().batch_size())
82            .with_header(header);
83
84        let config = StreamConfig::new(Arc::new(source))
85            .with_order(cmd.order_exprs.clone())
86            .with_constraints(cmd.constraints.clone());
87
88        Ok(Arc::new(StreamTable(Arc::new(config))))
89    }
90}
91
92/// The data encoding for [`StreamTable`]
93#[derive(Debug, Clone)]
94pub enum StreamEncoding {
95    /// CSV records
96    Csv,
97    /// Newline-delimited JSON records
98    Json,
99}
100
101impl FromStr for StreamEncoding {
102    type Err = DataFusionError;
103
104    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
105        match s.to_ascii_lowercase().as_str() {
106            "csv" => Ok(Self::Csv),
107            "json" => Ok(Self::Json),
108            _ => plan_err!("Unrecognized StreamEncoding {}", s),
109        }
110    }
111}
112
113/// The StreamProvider trait is used as a generic interface for reading and writing from streaming
114/// data sources (such as FIFO, Websocket, Kafka, etc.).  Implementations of the provider are
115/// responsible for providing a `RecordBatchReader` and optionally a `RecordBatchWriter`.
116pub trait StreamProvider: std::fmt::Debug + Send + Sync {
117    /// Get a reference to the schema for this stream
118    fn schema(&self) -> &SchemaRef;
119    /// Provide `RecordBatchReader`
120    fn reader(&self) -> Result<Box<dyn RecordBatchReader>>;
121    /// Provide `RecordBatchWriter`
122    fn writer(&self) -> Result<Box<dyn RecordBatchWriter>> {
123        unimplemented!()
124    }
125    /// Display implementation when using as a DataSink
126    fn stream_write_display(
127        &self,
128        t: DisplayFormatType,
129        f: &mut Formatter,
130    ) -> std::fmt::Result;
131}
132
133/// Stream data from the file at `location`
134///
135/// * Data will be read sequentially from the provided `location`
136/// * New data will be appended to the end of the file
137///
138/// The encoding can be configured with [`Self::with_encoding`] and
139/// defaults to [`StreamEncoding::Csv`]
140#[derive(Debug)]
141pub struct FileStreamProvider {
142    location: PathBuf,
143    encoding: StreamEncoding,
144    /// Get a reference to the schema for this file stream
145    pub schema: SchemaRef,
146    header: bool,
147    batch_size: usize,
148}
149
150impl FileStreamProvider {
151    /// Stream data from the file at `location`
152    ///
153    /// * Data will be read sequentially from the provided `location`
154    /// * New data will be appended to the end of the file
155    ///
156    /// The encoding can be configured with [`Self::with_encoding`] and
157    /// defaults to [`StreamEncoding::Csv`]
158    pub fn new_file(schema: SchemaRef, location: PathBuf) -> Self {
159        Self {
160            schema,
161            location,
162            batch_size: 1024,
163            encoding: StreamEncoding::Csv,
164            header: false,
165        }
166    }
167
168    /// Set the batch size (the number of rows to load at one time)
169    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
170        self.batch_size = batch_size;
171        self
172    }
173
174    /// Specify whether the file has a header (only applicable for [`StreamEncoding::Csv`])
175    pub fn with_header(mut self, header: bool) -> Self {
176        self.header = header;
177        self
178    }
179
180    /// Specify an encoding for the stream
181    pub fn with_encoding(mut self, encoding: StreamEncoding) -> Self {
182        self.encoding = encoding;
183        self
184    }
185}
186
187impl StreamProvider for FileStreamProvider {
188    fn schema(&self) -> &SchemaRef {
189        &self.schema
190    }
191
192    fn reader(&self) -> Result<Box<dyn RecordBatchReader>> {
193        let file = File::open(&self.location)?;
194        let schema = Arc::clone(&self.schema);
195        match &self.encoding {
196            StreamEncoding::Csv => {
197                let reader = arrow::csv::ReaderBuilder::new(schema)
198                    .with_header(self.header)
199                    .with_batch_size(self.batch_size)
200                    .build(file)?;
201
202                Ok(Box::new(reader))
203            }
204            StreamEncoding::Json => {
205                let reader = arrow::json::ReaderBuilder::new(schema)
206                    .with_batch_size(self.batch_size)
207                    .build(BufReader::new(file))?;
208
209                Ok(Box::new(reader))
210            }
211        }
212    }
213
214    fn writer(&self) -> Result<Box<dyn RecordBatchWriter>> {
215        match &self.encoding {
216            StreamEncoding::Csv => {
217                let header = self.header && !self.location.exists();
218                let file = OpenOptions::new()
219                    .create(true)
220                    .append(true)
221                    .open(&self.location)?;
222                let writer = arrow::csv::WriterBuilder::new()
223                    .with_header(header)
224                    .build(file);
225
226                Ok(Box::new(writer))
227            }
228            StreamEncoding::Json => {
229                let file = OpenOptions::new()
230                    .create(true)
231                    .append(true)
232                    .open(&self.location)?;
233                Ok(Box::new(arrow::json::LineDelimitedWriter::new(file)))
234            }
235        }
236    }
237
238    fn stream_write_display(
239        &self,
240        _t: DisplayFormatType,
241        f: &mut Formatter,
242    ) -> std::fmt::Result {
243        f.debug_struct("StreamWrite")
244            .field("location", &self.location)
245            .field("batch_size", &self.batch_size)
246            .field("encoding", &self.encoding)
247            .field("header", &self.header)
248            .finish_non_exhaustive()
249    }
250}
251
252/// The configuration for a [`StreamTable`]
253#[derive(Debug)]
254pub struct StreamConfig {
255    source: Arc<dyn StreamProvider>,
256    order: Vec<Vec<SortExpr>>,
257    constraints: Constraints,
258}
259
260impl StreamConfig {
261    /// Create a new `StreamConfig` from a `StreamProvider`
262    pub fn new(source: Arc<dyn StreamProvider>) -> Self {
263        Self {
264            source,
265            order: vec![],
266            constraints: Constraints::default(),
267        }
268    }
269
270    /// Specify a sort order for the stream
271    pub fn with_order(mut self, order: Vec<Vec<SortExpr>>) -> Self {
272        self.order = order;
273        self
274    }
275
276    /// Assign constraints
277    pub fn with_constraints(mut self, constraints: Constraints) -> Self {
278        self.constraints = constraints;
279        self
280    }
281
282    fn reader(&self) -> Result<Box<dyn RecordBatchReader>> {
283        self.source.reader()
284    }
285
286    fn writer(&self) -> Result<Box<dyn RecordBatchWriter>> {
287        self.source.writer()
288    }
289}
290
291/// A [`TableProvider`] for an unbounded stream source
292///
293/// Currently only reading from / appending to a single file in-place is supported, but
294/// other stream sources and sinks may be added in future.
295///
296/// Applications looking to read/write datasets comprising multiple files, e.g. [Hadoop]-style
297/// data stored in object storage, should instead consider [`ListingTable`].
298///
299/// [Hadoop]: https://hadoop.apache.org/
300/// [`ListingTable`]: https://docs.rs/datafusion/latest/datafusion/datasource/listing/struct.ListingTable.html
301#[derive(Debug)]
302pub struct StreamTable(Arc<StreamConfig>);
303
304impl StreamTable {
305    /// Create a new [`StreamTable`] for the given [`StreamConfig`]
306    pub fn new(config: Arc<StreamConfig>) -> Self {
307        Self(config)
308    }
309}
310
311#[async_trait]
312impl TableProvider for StreamTable {
313    fn schema(&self) -> SchemaRef {
314        Arc::clone(self.0.source.schema())
315    }
316
317    fn constraints(&self) -> Option<&Constraints> {
318        Some(&self.0.constraints)
319    }
320
321    fn table_type(&self) -> TableType {
322        TableType::Base
323    }
324
325    async fn scan(
326        &self,
327        state: &dyn Session,
328        projection: Option<&Vec<usize>>,
329        _filters: &[Expr],
330        limit: Option<usize>,
331    ) -> Result<Arc<dyn ExecutionPlan>> {
332        let projected_schema = match projection {
333            Some(p) => {
334                let projected = Arc::new(self.0.source.schema().project(p)?);
335                create_lex_ordering(&projected, &self.0.order, state.execution_props())?
336            }
337            None => create_lex_ordering(
338                self.0.source.schema(),
339                &self.0.order,
340                state.execution_props(),
341            )?,
342        };
343
344        Ok(Arc::new(StreamingTableExec::try_new(
345            Arc::clone(self.0.source.schema()),
346            vec![Arc::new(StreamRead(Arc::clone(&self.0))) as _],
347            projection,
348            projected_schema,
349            true,
350            limit,
351        )?))
352    }
353
354    async fn insert_into(
355        &self,
356        _state: &dyn Session,
357        input: Arc<dyn ExecutionPlan>,
358        _insert_op: InsertOp,
359    ) -> Result<Arc<dyn ExecutionPlan>> {
360        let schema = self.0.source.schema();
361        let orders =
362            create_lex_ordering(schema, &self.0.order, _state.execution_props())?;
363        // It is sufficient to pass only one of the equivalent orderings:
364        let ordering = orders.into_iter().next().map(Into::into);
365
366        Ok(Arc::new(DataSinkExec::new(
367            input,
368            Arc::new(StreamWrite(Arc::clone(&self.0))),
369            ordering,
370        )))
371    }
372}
373
374#[derive(Debug)]
375struct StreamRead(Arc<StreamConfig>);
376
377impl PartitionStream for StreamRead {
378    fn schema(&self) -> &SchemaRef {
379        self.0.source.schema()
380    }
381
382    fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
383        let config = Arc::clone(&self.0);
384        let schema = Arc::clone(self.0.source.schema());
385        let mut builder = RecordBatchReceiverStreamBuilder::new(schema, 2);
386        let tx = builder.tx();
387        builder.spawn_blocking(move || {
388            let reader = config.reader()?;
389            for b in reader {
390                if tx.blocking_send(b.map_err(Into::into)).is_err() {
391                    break;
392                }
393            }
394            Ok(())
395        });
396        builder.build()
397    }
398}
399
400#[derive(Debug)]
401struct StreamWrite(Arc<StreamConfig>);
402
403impl DisplayAs for StreamWrite {
404    fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result {
405        self.0.source.stream_write_display(t, f)
406    }
407}
408
409#[async_trait]
410impl DataSink for StreamWrite {
411    fn schema(&self) -> &SchemaRef {
412        self.0.source.schema()
413    }
414
415    async fn write_all(
416        &self,
417        mut data: SendableRecordBatchStream,
418        _context: &Arc<TaskContext>,
419    ) -> Result<u64> {
420        let config = Arc::clone(&self.0);
421        let (sender, mut receiver) = tokio::sync::mpsc::channel::<RecordBatch>(2);
422        // Note: FIFO Files support poll so this could use AsyncFd
423        let write_task = SpawnedTask::spawn_blocking(move || {
424            let mut count = 0_u64;
425            let mut writer = config.writer()?;
426            while let Some(batch) = receiver.blocking_recv() {
427                count += batch.num_rows() as u64;
428                writer.write(&batch)?;
429            }
430            Ok(count)
431        });
432
433        while let Some(b) = data.next().await.transpose()? {
434            if sender.send(b).await.is_err() {
435                break;
436            }
437        }
438        drop(sender);
439        write_task
440            .join_unwind()
441            .await
442            .map_err(|e| DataFusionError::ExecutionJoin(Box::new(e)))?
443    }
444}