datafusion_datasource_arrow/
source.rs1use std::io::Cursor;
35use std::sync::Arc;
36
37use datafusion_datasource::{TableSchema, as_file_source};
38
39use arrow::buffer::Buffer;
40use arrow::ipc::reader::{FileDecoder, FileReader, StreamReader};
41use datafusion_common::error::Result;
42use datafusion_common::exec_datafusion_err;
43use datafusion_common::tree_node::TreeNodeRecursion;
44use datafusion_datasource::PartitionedFile;
45use datafusion_datasource::file::FileSource;
46use datafusion_datasource::file_scan_config::FileScanConfig;
47use datafusion_datasource::projection::{ProjectionOpener, SplitProjection};
48use datafusion_physical_expr_common::sort_expr::LexOrdering;
49use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
50use datafusion_physical_plan::projection::ProjectionExprs;
51
52use datafusion_datasource::file_stream::FileOpenFuture;
53use datafusion_datasource::file_stream::FileOpener;
54use futures::StreamExt;
55use itertools::Itertools;
56use object_store::{GetOptions, GetRange, GetResultPayload, ObjectStore, ObjectStoreExt};
57
58#[derive(Clone, Copy, Debug)]
60enum ArrowFormat {
61 File,
63 Stream,
65}
66
67pub(crate) struct ArrowStreamFileOpener {
69 object_store: Arc<dyn ObjectStore>,
70 projection: Option<Vec<usize>>,
71}
72
73impl FileOpener for ArrowStreamFileOpener {
74 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
75 if partitioned_file.range.is_some() {
76 return Err(exec_datafusion_err!(
77 "ArrowStreamFileOpener does not support range-based reading"
78 ));
79 }
80 let object_store = Arc::clone(&self.object_store);
81 let projection = self.projection.clone();
82
83 Ok(Box::pin(async move {
84 let r = object_store
85 .get(&partitioned_file.object_meta.location)
86 .await?;
87
88 let stream = match r.payload {
89 #[cfg(not(target_arch = "wasm32"))]
90 GetResultPayload::File(file, _) => futures::stream::iter(
91 StreamReader::try_new(file.try_clone()?, projection.clone())?,
92 )
93 .map(|r| r.map_err(Into::into))
94 .boxed(),
95 GetResultPayload::Stream(_) => {
96 let bytes = r.bytes().await?;
97 let cursor = Cursor::new(bytes);
98 futures::stream::iter(StreamReader::try_new(
99 cursor,
100 projection.clone(),
101 )?)
102 .map(|r| r.map_err(Into::into))
103 .boxed()
104 }
105 };
106
107 Ok(stream)
108 }))
109 }
110}
111
112pub(crate) struct ArrowFileOpener {
114 object_store: Arc<dyn ObjectStore>,
115 projection: Option<Vec<usize>>,
116}
117
118impl FileOpener for ArrowFileOpener {
119 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
120 let object_store = Arc::clone(&self.object_store);
121 let projection = self.projection.clone();
122
123 Ok(Box::pin(async move {
124 let range = partitioned_file.range.clone();
125 match range {
126 None => {
127 let r = object_store
128 .get(&partitioned_file.object_meta.location)
129 .await?;
130 let stream = match r.payload {
131 #[cfg(not(target_arch = "wasm32"))]
132 GetResultPayload::File(file, _) => futures::stream::iter(
133 FileReader::try_new(file.try_clone()?, projection.clone())?,
134 )
135 .map(|r| r.map_err(Into::into))
136 .boxed(),
137 GetResultPayload::Stream(_) => {
138 let bytes = r.bytes().await?;
139 let cursor = Cursor::new(bytes);
140 futures::stream::iter(FileReader::try_new(
141 cursor,
142 projection.clone(),
143 )?)
144 .map(|r| r.map_err(Into::into))
145 .boxed()
146 }
147 };
148
149 Ok(stream)
150 }
151 Some(range) => {
152 let get_option = GetOptions {
155 range: Some(GetRange::Suffix(10)),
156 ..Default::default()
157 };
158 let get_result = object_store
159 .get_opts(&partitioned_file.object_meta.location, get_option)
160 .await?;
161 let footer_len_buf = get_result.bytes().await?;
162 let footer_len = arrow_ipc::reader::read_footer_length(
163 footer_len_buf[..].try_into().unwrap(),
164 )?;
165 let get_option = GetOptions {
167 range: Some(GetRange::Suffix(10 + (footer_len as u64))),
168 ..Default::default()
169 };
170 let get_result = object_store
171 .get_opts(&partitioned_file.object_meta.location, get_option)
172 .await?;
173 let footer_buf = get_result.bytes().await?;
174 let footer = arrow_ipc::root_as_footer(
175 footer_buf[..footer_len].try_into().unwrap(),
176 )
177 .map_err(|err| {
178 exec_datafusion_err!("Unable to get root as footer: {err:?}")
179 })?;
180 let schema =
182 arrow_ipc::convert::fb_to_schema(footer.schema().unwrap());
183 let mut decoder = FileDecoder::new(schema.into(), footer.version());
184 if let Some(projection) = projection {
185 decoder = decoder.with_projection(projection);
186 }
187 let dict_ranges = footer
188 .dictionaries()
189 .iter()
190 .flatten()
191 .map(|block| {
192 let block_len =
193 block.bodyLength() as u64 + block.metaDataLength() as u64;
194 let block_offset = block.offset() as u64;
195 block_offset..block_offset + block_len
196 })
197 .collect_vec();
198 let dict_results = object_store
199 .get_ranges(&partitioned_file.object_meta.location, &dict_ranges)
200 .await?;
201 for (dict_block, dict_result) in
202 footer.dictionaries().iter().flatten().zip(dict_results)
203 {
204 decoder
205 .read_dictionary(dict_block, &Buffer::from(dict_result))?;
206 }
207
208 let recordbatches = footer
210 .recordBatches()
211 .iter()
212 .flatten()
213 .filter(|block| {
214 let block_offset = block.offset() as u64;
215 block_offset >= range.start as u64
216 && block_offset < range.end as u64
217 })
218 .copied()
219 .collect_vec();
220
221 let recordbatch_ranges = recordbatches
222 .iter()
223 .map(|block| {
224 let block_len =
225 block.bodyLength() as u64 + block.metaDataLength() as u64;
226 let block_offset = block.offset() as u64;
227 block_offset..block_offset + block_len
228 })
229 .collect_vec();
230
231 let recordbatch_results = object_store
232 .get_ranges(
233 &partitioned_file.object_meta.location,
234 &recordbatch_ranges,
235 )
236 .await?;
237
238 let stream = futures::stream::iter(
239 recordbatches
240 .into_iter()
241 .zip(recordbatch_results)
242 .filter_map(move |(block, data)| {
243 decoder
244 .read_record_batch(&block, &Buffer::from(data))
245 .transpose()
246 }),
247 )
248 .map(|r| r.map_err(Into::into))
249 .boxed();
250
251 Ok(stream)
252 }
253 }
254 }))
255 }
256}
257
258#[derive(Clone)]
260pub struct ArrowSource {
261 format: ArrowFormat,
262 metrics: ExecutionPlanMetricsSet,
263 projection: SplitProjection,
264 table_schema: TableSchema,
265}
266
267impl ArrowSource {
268 pub fn new_file_source(table_schema: impl Into<TableSchema>) -> Self {
270 let table_schema = table_schema.into();
271 Self {
272 format: ArrowFormat::File,
273 metrics: ExecutionPlanMetricsSet::new(),
274 projection: SplitProjection::unprojected(&table_schema),
275 table_schema,
276 }
277 }
278
279 pub fn new_stream_file_source(table_schema: impl Into<TableSchema>) -> Self {
281 let table_schema = table_schema.into();
282 Self {
283 format: ArrowFormat::Stream,
284 metrics: ExecutionPlanMetricsSet::new(),
285 projection: SplitProjection::unprojected(&table_schema),
286 table_schema,
287 }
288 }
289}
290
291impl FileSource for ArrowSource {
292 fn create_file_opener(
293 &self,
294 object_store: Arc<dyn ObjectStore>,
295 _base_config: &FileScanConfig,
296 _partition: usize,
297 ) -> Result<Arc<dyn FileOpener>> {
298 let split_projection = self.projection.clone();
299
300 let opener: Arc<dyn FileOpener> = match self.format {
301 ArrowFormat::File => Arc::new(ArrowFileOpener {
302 object_store,
303 projection: Some(split_projection.file_indices.clone()),
304 }),
305 ArrowFormat::Stream => Arc::new(ArrowStreamFileOpener {
306 object_store,
307 projection: Some(split_projection.file_indices.clone()),
308 }),
309 };
310 ProjectionOpener::try_new(
311 split_projection,
312 opener,
313 self.table_schema.file_schema(),
314 )
315 }
316
317 fn with_batch_size(&self, _batch_size: usize) -> Arc<dyn FileSource> {
318 Arc::new(Self { ..self.clone() })
319 }
320
321 fn metrics(&self) -> &ExecutionPlanMetricsSet {
322 &self.metrics
323 }
324
325 fn file_type(&self) -> &str {
326 match self.format {
327 ArrowFormat::File => "arrow",
328 ArrowFormat::Stream => "arrow_stream",
329 }
330 }
331
332 fn repartitioned(
333 &self,
334 target_partitions: usize,
335 repartition_file_min_size: usize,
336 output_ordering: Option<LexOrdering>,
337 config: &FileScanConfig,
338 ) -> Result<Option<FileScanConfig>> {
339 match self.format {
340 ArrowFormat::Stream => {
341 Ok(None)
351 }
352 ArrowFormat::File => {
353 use datafusion_datasource::file_groups::FileGroupPartitioner;
355
356 if config.file_compression_type.is_compressed() {
357 return Ok(None);
358 }
359
360 let repartitioned_file_groups_option = FileGroupPartitioner::new()
361 .with_target_partitions(target_partitions)
362 .with_repartition_file_min_size(repartition_file_min_size)
363 .with_preserve_order_within_groups(output_ordering.is_some())
364 .repartition_file_groups(&config.file_groups);
365
366 if let Some(repartitioned_file_groups) = repartitioned_file_groups_option
367 {
368 let mut source = config.clone();
369 source.file_groups = repartitioned_file_groups;
370 return Ok(Some(source));
371 }
372 Ok(None)
373 }
374 }
375 }
376
377 fn table_schema(&self) -> &TableSchema {
378 &self.table_schema
379 }
380
381 fn try_pushdown_projection(
382 &self,
383 projection: &ProjectionExprs,
384 ) -> Result<Option<Arc<dyn FileSource>>> {
385 let mut source = self.clone();
386 source.projection = SplitProjection::new(
387 self.table_schema().file_schema(),
388 &source.projection.source.try_merge(projection)?,
389 );
390 Ok(Some(Arc::new(source)))
391 }
392
393 fn projection(&self) -> Option<&ProjectionExprs> {
394 Some(&self.projection.source)
395 }
396
397 fn apply_expressions(
398 &self,
399 f: &mut dyn FnMut(
400 &Arc<dyn datafusion_physical_plan::PhysicalExpr>,
401 ) -> Result<TreeNodeRecursion>,
402 ) -> Result<TreeNodeRecursion> {
403 datafusion_physical_plan::apply_expression_roots(self.projection.source.iter(), f)
404 }
405
406 #[cfg(feature = "proto")]
411 fn try_to_proto(
412 &self,
413 base: &FileScanConfig,
414 ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
415 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
416 use datafusion_proto_models::protobuf;
417 use protobuf::physical_plan_node::PhysicalPlanType;
418
419 Ok(Some(protobuf::PhysicalPlanNode {
420 physical_plan_type: Some(PhysicalPlanType::ArrowScan(
421 protobuf::ArrowScanExecNode {
422 base_conf: Some(base.try_to_proto(ctx)?),
423 },
424 )),
425 }))
426 }
427}
428
429#[cfg(feature = "proto")]
430impl ArrowSource {
431 pub fn try_from_proto(
436 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
437 ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
438 ) -> Result<Arc<dyn datafusion_physical_plan::ExecutionPlan>> {
439 use datafusion_datasource::file_scan_config::FileScanConfig;
440 use datafusion_datasource::source::DataSourceExec;
441 use datafusion_proto_models::protobuf;
442
443 let scan = match &node.physical_plan_type {
444 Some(protobuf::physical_plan_node::PhysicalPlanType::ArrowScan(scan)) => scan,
445 _ => {
446 return datafusion_common::internal_err!(
447 "PhysicalPlanNode is not an ArrowScan"
448 );
449 }
450 };
451
452 let base_conf = scan.base_conf.as_ref().ok_or_else(|| {
453 datafusion_common::internal_datafusion_err!(
454 "ArrowScanExecNode is missing required field 'base_conf'"
455 )
456 })?;
457
458 let table_schema = FileScanConfig::parse_table_schema_from_proto(base_conf)?;
459 let source = Arc::new(ArrowSource::new_file_source(table_schema));
460 let scan_conf = FileScanConfig::try_from_proto(base_conf, ctx, source)?;
461 Ok(DataSourceExec::from_data_source(scan_conf))
462 }
463}
464
465pub struct ArrowOpener {
467 pub inner: Arc<dyn FileOpener>,
468}
469
470impl FileOpener for ArrowOpener {
471 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
472 self.inner.open(partitioned_file)
473 }
474}
475
476impl ArrowOpener {
477 pub fn new(inner: Arc<dyn FileOpener>) -> Self {
479 Self { inner }
480 }
481
482 pub fn new_file_opener(
483 object_store: Arc<dyn ObjectStore>,
484 projection: Option<Vec<usize>>,
485 ) -> Self {
486 Self {
487 inner: Arc::new(ArrowFileOpener {
488 object_store,
489 projection,
490 }),
491 }
492 }
493
494 pub fn new_stream_file_opener(
495 object_store: Arc<dyn ObjectStore>,
496 projection: Option<Vec<usize>>,
497 ) -> Self {
498 Self {
499 inner: Arc::new(ArrowStreamFileOpener {
500 object_store,
501 projection,
502 }),
503 }
504 }
505}
506
507impl From<ArrowSource> for Arc<dyn FileSource> {
508 fn from(source: ArrowSource) -> Self {
509 as_file_source(source)
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use std::{fs::File, io::Read};
516
517 use arrow::datatypes::{DataType, Field, Schema};
518 use arrow_ipc::reader::{FileReader, StreamReader};
519 use bytes::Bytes;
520 use datafusion_datasource::file_scan_config::FileScanConfigBuilder;
521 use datafusion_execution::object_store::ObjectStoreUrl;
522 use object_store::memory::InMemory;
523
524 use super::*;
525
526 #[tokio::test]
527 async fn test_file_opener_without_ranges() -> Result<()> {
528 for filename in ["example.arrow", "example_stream.arrow"] {
529 let path = format!("tests/data/{filename}");
530 let path_str = path.as_str();
531 let mut file = File::open(path_str)?;
532 let file_size = file.metadata()?.len();
533
534 let mut buffer = Vec::new();
535 file.read_to_end(&mut buffer)?;
536 let bytes = Bytes::from(buffer);
537
538 let object_store = Arc::new(InMemory::new());
539 let partitioned_file = PartitionedFile::new(filename, file_size);
540 object_store
541 .put(&partitioned_file.object_meta.location, bytes.into())
542 .await?;
543
544 let schema = match FileReader::try_new(File::open(path_str)?, None) {
545 Ok(reader) => reader.schema(),
546 Err(_) => StreamReader::try_new(File::open(path_str)?, None)?.schema(),
547 };
548
549 let source: Arc<dyn FileSource> = if filename.contains("stream") {
550 Arc::new(ArrowSource::new_stream_file_source(schema))
551 } else {
552 Arc::new(ArrowSource::new_file_source(schema))
553 };
554
555 let scan_config = FileScanConfigBuilder::new(
556 ObjectStoreUrl::local_filesystem(),
557 source.clone(),
558 )
559 .build();
560
561 let file_opener = source.create_file_opener(object_store, &scan_config, 0)?;
562 let mut stream = file_opener.open(partitioned_file)?.await?;
563
564 assert!(stream.next().await.is_some());
565 }
566
567 Ok(())
568 }
569
570 #[tokio::test]
571 async fn test_file_opener_with_ranges() -> Result<()> {
572 let filename = "example.arrow";
573 let path = format!("tests/data/{filename}");
574 let path_str = path.as_str();
575 let mut file = File::open(path_str)?;
576 let file_size = file.metadata()?.len();
577
578 let mut buffer = Vec::new();
579 file.read_to_end(&mut buffer)?;
580 let bytes = Bytes::from(buffer);
581
582 let object_store = Arc::new(InMemory::new());
583 let partitioned_file = PartitionedFile::new_with_range(
584 filename.into(),
585 file_size,
586 0,
587 (file_size - 1) as i64,
588 );
589 object_store
590 .put(&partitioned_file.object_meta.location, bytes.into())
591 .await?;
592
593 let schema = FileReader::try_new(File::open(path_str)?, None)?.schema();
594
595 let source = Arc::new(ArrowSource::new_file_source(schema));
596
597 let scan_config = FileScanConfigBuilder::new(
598 ObjectStoreUrl::local_filesystem(),
599 source.clone(),
600 )
601 .build();
602
603 let file_opener = source.create_file_opener(object_store, &scan_config, 0)?;
604 let mut stream = file_opener.open(partitioned_file)?.await?;
605
606 assert!(stream.next().await.is_some());
607
608 Ok(())
609 }
610
611 #[tokio::test]
612 async fn test_stream_opener_errors_with_ranges() -> Result<()> {
613 let filename = "example_stream.arrow";
614 let path = format!("tests/data/{filename}");
615 let path_str = path.as_str();
616 let mut file = File::open(path_str)?;
617 let file_size = file.metadata()?.len();
618
619 let mut buffer = Vec::new();
620 file.read_to_end(&mut buffer)?;
621 let bytes = Bytes::from(buffer);
622
623 let object_store = Arc::new(InMemory::new());
624 let partitioned_file = PartitionedFile::new_with_range(
625 filename.into(),
626 file_size,
627 0,
628 (file_size - 1) as i64,
629 );
630 object_store
631 .put(&partitioned_file.object_meta.location, bytes.into())
632 .await?;
633
634 let schema = StreamReader::try_new(File::open(path_str)?, None)?.schema();
635
636 let source = Arc::new(ArrowSource::new_stream_file_source(schema));
637
638 let scan_config = FileScanConfigBuilder::new(
639 ObjectStoreUrl::local_filesystem(),
640 source.clone(),
641 )
642 .build();
643
644 let file_opener = source.create_file_opener(object_store, &scan_config, 0)?;
645 let result = file_opener.open(partitioned_file);
646 assert!(result.is_err());
647
648 Ok(())
649 }
650
651 #[tokio::test]
652 async fn test_arrow_stream_repartitioning_not_supported() -> Result<()> {
653 let schema =
654 Arc::new(Schema::new(vec![Field::new("f0", DataType::Int64, false)]));
655 let source = ArrowSource::new_stream_file_source(schema);
656
657 let config = FileScanConfigBuilder::new(
658 ObjectStoreUrl::local_filesystem(),
659 Arc::new(source.clone()) as Arc<dyn FileSource>,
660 )
661 .build();
662
663 for target_partitions in [2, 4, 8, 16] {
664 let result =
665 source.repartitioned(target_partitions, 1024 * 1024, None, &config)?;
666
667 assert!(
668 result.is_none(),
669 "Stream format should not support repartitioning with {target_partitions} partitions",
670 );
671 }
672
673 Ok(())
674 }
675
676 #[tokio::test]
677 async fn test_stream_opener_with_projection() -> Result<()> {
678 let filename = "example_stream.arrow";
679 let path = format!("tests/data/{filename}");
680 let path_str = path.as_str();
681 let mut file = File::open(path_str)?;
682 let file_size = file.metadata()?.len();
683
684 let mut buffer = Vec::new();
685 file.read_to_end(&mut buffer)?;
686 let bytes = Bytes::from(buffer);
687
688 let object_store = Arc::new(InMemory::new());
689 let partitioned_file = PartitionedFile::new(filename, file_size);
690 object_store
691 .put(&partitioned_file.object_meta.location, bytes.into())
692 .await?;
693
694 let opener = ArrowStreamFileOpener {
695 object_store,
696 projection: Some(vec![0]), };
698
699 let mut stream = opener.open(partitioned_file)?.await?;
700
701 if let Some(batch) = stream.next().await {
702 let batch = batch?;
703 assert_eq!(
704 batch.num_columns(),
705 1,
706 "Projection should result in 1 column"
707 );
708 } else {
709 panic!("Expected at least one batch");
710 }
711
712 Ok(())
713 }
714}