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}