Skip to main content

datafusion_datasource/
file.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//! Common behaviors that every file format needs to implement
19
20use std::any::Any;
21use std::fmt;
22use std::fmt::Formatter;
23use std::sync::Arc;
24
25use crate::file_groups::FileGroupPartitioner;
26use crate::file_scan_config::FileScanConfig;
27use crate::file_stream::FileOpener;
28use crate::morsel::{FileOpenerMorselizer, Morselizer};
29#[expect(deprecated)]
30use crate::schema_adapter::SchemaAdapterFactory;
31use datafusion_common::config::ConfigOptions;
32use datafusion_common::tree_node::TreeNodeRecursion;
33use datafusion_common::{Result, not_impl_err};
34use datafusion_physical_expr::projection::ProjectionExprs;
35use datafusion_physical_expr::{EquivalenceProperties, LexOrdering, PhysicalExpr};
36use datafusion_physical_plan::DisplayFormatType;
37use datafusion_physical_plan::SortOrderPushdownResult;
38use datafusion_physical_plan::filter_pushdown::{FilterPushdownPropagation, PushedDown};
39use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
40
41use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
42use object_store::ObjectStore;
43
44/// Helper function to convert any type implementing [`FileSource`] to `Arc<dyn FileSource>`
45pub fn as_file_source<T: FileSource + 'static>(source: T) -> Arc<dyn FileSource> {
46    Arc::new(source)
47}
48
49/// File format specific behaviors for [`DataSource`]
50///
51/// # Schema information
52/// There are two important schemas for a [`FileSource`]:
53/// 1. [`Self::table_schema`] -- the schema for the overall table
54///    (file data plus partition columns)
55/// 2. The logical output schema, comprised of [`Self::table_schema`] with
56///    [`Self::projection`] applied
57///
58/// See more details on specific implementations:
59/// * [`ArrowSource`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/struct.ArrowSource.html)
60/// * [`AvroSource`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/struct.AvroSource.html)
61/// * [`CsvSource`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/struct.CsvSource.html)
62/// * [`JsonSource`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/struct.JsonSource.html)
63/// * [`ParquetSource`](https://docs.rs/datafusion/latest/datafusion/datasource/physical_plan/struct.ParquetSource.html)
64///
65/// [`DataSource`]: crate::source::DataSource
66pub trait FileSource: Any + Send + Sync {
67    /// Creates a `dyn FileOpener` based on given parameters.
68    ///
69    /// Note: File sources with a native morsel implementation should return an
70    /// error from this method and implementing [`Self::create_morselizer`] instead.
71    fn create_file_opener(
72        &self,
73        object_store: Arc<dyn ObjectStore>,
74        base_config: &FileScanConfig,
75        partition: usize,
76    ) -> Result<Arc<dyn FileOpener>>;
77
78    /// Creates a `dyn Morselizer` based on given parameters.
79    ///
80    /// The default implementation preserves existing behavior by adapting the
81    /// legacy [`FileOpener`] API into a [`Morselizer`].
82    ///
83    /// It is preferred to implement the [`Morselizer`] API directly by
84    /// implementing this method.
85    fn create_morselizer(
86        &self,
87        object_store: Arc<dyn ObjectStore>,
88        base_config: &FileScanConfig,
89        partition: usize,
90    ) -> Result<Box<dyn Morselizer>> {
91        let opener = self.create_file_opener(object_store, base_config, partition)?;
92        Ok(Box::new(FileOpenerMorselizer::new(opener)))
93    }
94
95    /// Returns the table schema for the overall table (including partition columns, if any)
96    ///
97    /// This method returns the unprojected schema: the full schema of the data
98    /// without [`Self::projection`] applied.
99    ///
100    /// The output schema of this `FileSource` is this TableSchema
101    /// with [`Self::projection`] applied.
102    ///
103    /// Use [`ProjectionExprs::project_schema`] to get the projected schema
104    /// after applying the projection.
105    fn table_schema(&self) -> &crate::table_schema::TableSchema;
106
107    /// Initialize new type with batch size configuration
108    fn with_batch_size(&self, batch_size: usize) -> Arc<dyn FileSource>;
109
110    /// Returns the filter expression that will be applied *during* the file scan.
111    ///
112    /// These expressions are in terms of the unprojected [`Self::table_schema`].
113    fn filter(&self) -> Option<Arc<dyn PhysicalExpr>> {
114        None
115    }
116
117    /// Return the projection that will be applied to the output stream on top
118    /// of [`Self::table_schema`].
119    ///
120    /// Note you can use [`ProjectionExprs::project_schema`] on the table
121    /// schema to get the effective output schema of this source.
122    fn projection(&self) -> Option<&ProjectionExprs> {
123        None
124    }
125
126    /// Return execution plan metrics
127    fn metrics(&self) -> &ExecutionPlanMetricsSet;
128
129    /// String representation of file source such as "csv", "json", "parquet"
130    fn file_type(&self) -> &str;
131
132    /// Format FileType specific information
133    fn fmt_extra(&self, _t: DisplayFormatType, _f: &mut Formatter) -> fmt::Result {
134        Ok(())
135    }
136
137    /// Returns whether this file source supports repartitioning files by byte ranges.
138    ///
139    /// When this returns `true`, files can be split into multiple partitions
140    /// based on byte offsets for parallel reading.
141    ///
142    /// When this returns `false`, files cannot be repartitioned (e.g., CSV files
143    /// with `newlines_in_values` enabled cannot be split because record boundaries
144    /// cannot be determined by byte offset alone).
145    ///
146    /// The default implementation returns `true`. File sources that cannot support
147    /// repartitioning should override this method.
148    fn supports_repartitioning(&self) -> bool {
149        true
150    }
151
152    /// If supported by the [`FileSource`], redistribute files across partitions
153    /// according to their size. Allows custom file formats to implement their
154    /// own repartitioning logic.
155    ///
156    /// The default implementation uses [`FileGroupPartitioner`]. See that
157    /// struct for more details.
158    fn repartitioned(
159        &self,
160        target_partitions: usize,
161        repartition_file_min_size: usize,
162        output_ordering: Option<LexOrdering>,
163        config: &FileScanConfig,
164    ) -> Result<Option<FileScanConfig>> {
165        if config.file_compression_type.is_compressed() || !self.supports_repartitioning()
166        {
167            return Ok(None);
168        }
169
170        let repartitioned_file_groups_option = FileGroupPartitioner::new()
171            .with_target_partitions(target_partitions)
172            .with_repartition_file_min_size(repartition_file_min_size)
173            .with_preserve_order_within_groups(output_ordering.is_some())
174            .repartition_file_groups(&config.file_groups);
175
176        if let Some(repartitioned_file_groups) = repartitioned_file_groups_option {
177            let mut source = config.clone();
178            source.file_groups = repartitioned_file_groups;
179            return Ok(Some(source));
180        }
181        Ok(None)
182    }
183
184    /// Try to push down filters into this FileSource.
185    ///
186    /// `filters` must be in terms of the unprojected table schema (file schema
187    /// plus partition columns), before any projection is applied.
188    ///
189    /// Any filters that this FileSource chooses to evaluate itself should be
190    /// returned as `PushedDown::Yes` in the result, along with a FileSource
191    /// instance that incorporates those filters. Such filters are logically
192    /// applied "during" the file scan, meaning they may refer to columns not
193    /// included in the final output projection.
194    ///
195    /// Filters that cannot be pushed down should be marked as `PushedDown::No`,
196    /// and will be evaluated by an execution plan after the file source.
197    ///
198    /// See [`ExecutionPlan::handle_child_pushdown_result`] for more details.
199    ///
200    /// [`ExecutionPlan::handle_child_pushdown_result`]: datafusion_physical_plan::ExecutionPlan::handle_child_pushdown_result
201    fn try_pushdown_filters(
202        &self,
203        filters: Vec<Arc<dyn PhysicalExpr>>,
204        _config: &ConfigOptions,
205    ) -> Result<FilterPushdownPropagation<Arc<dyn FileSource>>> {
206        Ok(FilterPushdownPropagation::with_parent_pushdown_result(
207            vec![PushedDown::No; filters.len()],
208        ))
209    }
210
211    /// Try to create a new FileSource that can produce data in the specified sort order.
212    ///
213    /// This method attempts to optimize data retrieval to match the requested ordering.
214    /// It receives both the requested ordering and equivalence properties that describe
215    /// the output data from this file source.
216    ///
217    /// # Parameters
218    /// * `order` - The requested sort ordering from the query
219    /// * `eq_properties` - Equivalence properties of the data that will be produced by this
220    ///   file source. These properties describe the ordering, constant columns, and other
221    ///   relationships in the output data, allowing the implementation to determine if
222    ///   optimizations like reversed scanning can help satisfy the requested ordering.
223    ///   This includes information about:
224    ///   - The file's natural ordering (from output_ordering in FileScanConfig)
225    ///   - Constant columns (e.g., from filters like `ticker = 'AAPL'`)
226    ///   - Monotonic functions (e.g., `extract_year_month(timestamp)`)
227    ///   - Other equivalence relationships
228    ///
229    /// # Examples
230    ///
231    /// ## Example 1: Simple reverse
232    /// ```text
233    /// File ordering: [a ASC, b DESC]
234    /// Requested:     [a DESC]
235    /// Reversed file: [a DESC, b ASC]
236    /// Result: Satisfies request (prefix match) → Inexact
237    /// ```
238    ///
239    /// ## Example 2: Monotonic function
240    /// ```text
241    /// File ordering: [extract_year_month(ts) ASC, ts ASC]
242    /// Requested:     [ts DESC]
243    /// Reversed file: [extract_year_month(ts) DESC, ts DESC]
244    /// Result: Through monotonicity, satisfies [ts DESC] → Inexact
245    /// ```
246    ///
247    /// # Returns
248    /// * `Exact` - Created a source that guarantees perfect ordering
249    /// * `Inexact` - Created a source optimized for ordering (e.g., reversed row groups) but not perfectly sorted
250    /// * `Unsupported` - Cannot optimize for this ordering
251    ///
252    /// # Deprecation / migration notes
253    /// - [`Self::try_reverse_output`] was renamed to this method and deprecated since `53.0.0`.
254    ///   Per DataFusion's deprecation guidelines, it will be removed in `59.0.0` or later
255    ///   (6 major versions or 6 months, whichever is longer).
256    /// - New implementations should override [`Self::try_pushdown_sort`] directly.
257    /// - For backwards compatibility, the default implementation of
258    ///   [`Self::try_pushdown_sort`] delegates to the deprecated
259    ///   [`Self::try_reverse_output`] until it is removed. After that point, the
260    ///   default implementation will return [`SortOrderPushdownResult::Unsupported`].
261    fn try_pushdown_sort(
262        &self,
263        order: &[PhysicalSortExpr],
264        eq_properties: &EquivalenceProperties,
265    ) -> Result<SortOrderPushdownResult<Arc<dyn FileSource>>> {
266        #[expect(deprecated)]
267        self.try_reverse_output(order, eq_properties)
268    }
269
270    /// Deprecated: Renamed to [`Self::try_pushdown_sort`].
271    #[deprecated(
272        since = "53.0.0",
273        note = "Renamed to try_pushdown_sort. This method was never limited to reversing output. It will be removed in 59.0.0 or later."
274    )]
275    fn try_reverse_output(
276        &self,
277        _order: &[PhysicalSortExpr],
278        _eq_properties: &EquivalenceProperties,
279    ) -> Result<SortOrderPushdownResult<Arc<dyn FileSource>>> {
280        Ok(SortOrderPushdownResult::Unsupported)
281    }
282
283    /// Reorder files in the shared work queue to optimize query performance.
284    ///
285    /// For example, TopK queries benefit from reading files with the best
286    /// statistics first, so the dynamic filter threshold tightens quickly.
287    ///
288    /// The default implementation returns files unchanged (no reordering).
289    fn reorder_files(
290        &self,
291        files: Vec<crate::PartitionedFile>,
292    ) -> Vec<crate::PartitionedFile> {
293        files
294    }
295
296    /// Try to push down a projection into this FileSource.
297    ///
298    /// `FileSource` implementations that support projection pushdown should
299    /// override this method and return a new `FileSource` instance with the
300    /// projection incorporated.
301    ///
302    /// If a `FileSource` does accept a projection it is expected to handle
303    /// the projection in it's entirety, including partition columns.
304    /// For example, the `FileSource` may translate that projection into a
305    /// file format specific projection (e.g. Parquet can push down struct field access,
306    /// some other file formats like Vortex can push down computed expressions into un-decoded data)
307    /// and also need to handle partition column projection (generally done by replacing partition column
308    /// references with literal values derived from each files partition values).
309    ///
310    /// Not all FileSource's can handle complex expression pushdowns. For example,
311    /// a CSV file source may only support simple column selections. In such cases,
312    /// the `FileSource` can use [`SplitProjection`] and [`ProjectionOpener`]
313    /// to split the projection into a pushdownable part and a non-pushdownable part.
314    /// These helpers also handle partition column projection.
315    ///
316    /// [`SplitProjection`]: crate::projection::SplitProjection
317    /// [`ProjectionOpener`]: crate::projection::ProjectionOpener
318    fn try_pushdown_projection(
319        &self,
320        _projection: &ProjectionExprs,
321    ) -> Result<Option<Arc<dyn FileSource>>> {
322        Ok(None)
323    }
324
325    /// Deprecated: Set optional schema adapter factory.
326    ///
327    /// `SchemaAdapterFactory` has been removed. Use `PhysicalExprAdapterFactory` instead.
328    /// See `upgrading.md` for more details.
329    #[deprecated(
330        since = "53.0.0",
331        note = "SchemaAdapterFactory has been removed. Use PhysicalExprAdapterFactory instead. See upgrading.md for more details."
332    )]
333    #[expect(deprecated)]
334    fn with_schema_adapter_factory(
335        &self,
336        _factory: Arc<dyn SchemaAdapterFactory>,
337    ) -> Result<Arc<dyn FileSource>> {
338        not_impl_err!(
339            "SchemaAdapterFactory has been removed. Use PhysicalExprAdapterFactory instead. See upgrading.md for more details."
340        )
341    }
342
343    /// Deprecated: Returns the current schema adapter factory if set.
344    ///
345    /// `SchemaAdapterFactory` has been removed. Use `PhysicalExprAdapterFactory` instead.
346    /// See `upgrading.md` for more details.
347    #[deprecated(
348        since = "53.0.0",
349        note = "SchemaAdapterFactory has been removed. Use PhysicalExprAdapterFactory instead. See upgrading.md for more details."
350    )]
351    #[expect(deprecated)]
352    fn schema_adapter_factory(&self) -> Option<Arc<dyn SchemaAdapterFactory>> {
353        None
354    }
355
356    /// Apply a function to all physical expressions used by this file source.
357    ///
358    /// This includes:
359    /// - Filter predicates (which may contain dynamic filters)
360    /// - Projection expressions
361    ///
362    /// The function `f` should be called once per expression unless the function returns
363    /// [`TreeNodeRecursion::Stop`] to stop iteration.
364    ///
365    /// See [`ExecutionPlan::apply_expressions`] for more details and implementation examples.
366    ///
367    /// [`ExecutionPlan::apply_expressions`]: datafusion_physical_plan::ExecutionPlan::apply_expressions
368    fn apply_expressions(
369        &self,
370        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
371    ) -> Result<TreeNodeRecursion>;
372
373    /// Serialize this file source into a full [`PhysicalPlanNode`] (a
374    /// `DataSourceExec` wrapping the `FileScanConfig`), if it knows how.
375    ///
376    /// `base` is the shared [`FileScanConfig`] this source is wrapped in; the
377    /// format-agnostic parts (file groups, schema, statistics, ordering,
378    /// projection, …) are encoded via
379    /// [`FileScanConfig::try_to_proto`](crate::file_scan_config::FileScanConfig::try_to_proto),
380    /// and the concrete source appends its format-specific fields (e.g. CSV
381    /// delimiter/quote) around it.
382    ///
383    /// * `Ok(None)` (the default) — this source has no proto hook yet; the
384    ///   caller falls back to the central downcast chain in `datafusion-proto`.
385    /// * `Ok(Some(node))` — fully serialized; the caller must not fall back.
386    ///
387    /// [`PhysicalPlanNode`]: datafusion_proto_models::protobuf::PhysicalPlanNode
388    /// [`FileScanConfig`]: crate::file_scan_config::FileScanConfig
389    #[cfg(feature = "proto")]
390    fn try_to_proto(
391        &self,
392        _base: &FileScanConfig,
393        _ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
394    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
395        Ok(None)
396    }
397}
398
399impl dyn FileSource {
400    /// Returns `true` if this source is of type `T`.
401    pub fn is<T: FileSource>(&self) -> bool {
402        (self as &dyn Any).is::<T>()
403    }
404
405    /// Attempts to downcast this source to a concrete type `T`.
406    pub fn downcast_ref<T: FileSource>(&self) -> Option<&T> {
407        (self as &dyn Any).downcast_ref()
408    }
409}