Skip to main content

delta_arrow_reader/
datafusion_provider.rs

1//! Optional DataFusion table-provider and registration surface.
2
3use std::{collections::HashSet, fmt, sync::Arc};
4
5use arrow::datatypes::SchemaRef;
6use async_trait::async_trait;
7use datafusion::{
8    catalog::Session,
9    common::{DataFusionError, Result as DataFusionResult},
10    datasource::{TableProvider, TableType},
11    execution::context::SessionContext,
12    logical_expr::{Expr, TableProviderFilterPushDown},
13    physical_plan::ExecutionPlan,
14};
15
16use crate::{
17    DeltaReaderBackend, DeltaReaderError, DeltaReaderExecutionOptions, DeltaTable,
18    datafusion_execution::create_datafusion_execution_plan,
19    datafusion_planning::{
20        DataFusionFilterCapabilities, plan_datafusion_filters, plan_datafusion_scan,
21    },
22    kernel::delta_predicate_to_kernel_pruning,
23    planning::{
24        DeltaScanPartitionTargetOptions, plan_row_predicate, plan_scan, validate_backend_available,
25    },
26};
27
28const TRACING_TARGET: &str = "delta_arrow_reader::datafusion";
29
30/// DataFusion-specific scan settings for one provider.
31#[derive(Debug, Clone, Default)]
32pub struct DeltaDataFusionScanOptions {
33    /// Reader execution settings used by each provider scan.
34    pub execution_options: DeltaReaderExecutionOptions,
35    /// Optional explicit scan partition target.
36    pub target_partitions: Option<usize>,
37}
38
39/// Immutable DataFusion provider for one loaded Delta table snapshot.
40///
41/// ```no_run
42/// use std::sync::Arc;
43/// use datafusion::prelude::SessionContext;
44/// use delta_arrow_reader::{
45///     DeltaDataFusionScanOptions, DeltaTableBuilder, DeltaTableProvider,
46/// };
47///
48/// # fn build_provider() -> Result<(), Box<dyn std::error::Error>> {
49/// let table = DeltaTableBuilder::new("/tmp/example-delta-table").load()?;
50/// let provider = DeltaTableProvider::try_new(
51///     table,
52///     DeltaDataFusionScanOptions::default(),
53/// )?;
54/// SessionContext::new().register_table("orders", Arc::new(provider))?;
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Clone)]
59pub struct DeltaTableProvider {
60    table: DeltaTable,
61    options: DeltaDataFusionScanOptions,
62    source_name: Option<String>,
63}
64
65impl DeltaTableProvider {
66    /// Creates a provider after validating its options and table protocol.
67    pub fn try_new(
68        table: DeltaTable,
69        options: DeltaDataFusionScanOptions,
70    ) -> Result<Self, DeltaReaderError> {
71        Self::try_new_with_source_name(table, options, None)
72    }
73
74    fn try_new_with_source_name(
75        table: DeltaTable,
76        options: DeltaDataFusionScanOptions,
77        source_name: Option<String>,
78    ) -> Result<Self, DeltaReaderError> {
79        options.execution_options.validate()?;
80        validate_backend_available(options.execution_options)?;
81        if options.target_partitions == Some(0) {
82            return Err(DeltaReaderError::InvalidConfiguration {
83                reason: "scan_partition_target_must_be_positive",
84            });
85        }
86        table.validate_protocol()?;
87        Ok(Self {
88            table,
89            options,
90            source_name,
91        })
92    }
93
94    fn plan(
95        &self,
96        state: &dyn Session,
97        projection: Option<&[usize]>,
98        filters: &[Expr],
99    ) -> Result<(Arc<dyn ExecutionPlan>, usize), DeltaReaderError> {
100        let _planning = tracing::debug_span!(
101            target: "delta_arrow_reader::profile",
102            "Delta scan planning"
103        )
104        .entered();
105        let partition_columns = self
106            .table
107            .partition_columns()
108            .iter()
109            .cloned()
110            .collect::<HashSet<_>>();
111        let filter_refs = filters.iter().collect::<Vec<_>>();
112        let planning = plan_datafusion_scan(
113            self.table.schema(),
114            &partition_columns,
115            projection,
116            &filter_refs,
117            DataFusionFilterCapabilities {
118                exact_predicate_evaluation: self.options.execution_options.reader_backend()
119                    == DeltaReaderBackend::NativeAsync,
120            },
121        )?;
122        if planning
123            .filters
124            .decisions
125            .iter()
126            .any(|decision| decision.pushdown == TableProviderFilterPushDown::Unsupported)
127        {
128            return Err(DeltaReaderError::UnsupportedPredicate {
129                reason: "datafusion_scan_contains_unsupported_filter",
130            });
131        }
132        let physical_projection = planning.projection.physical_projection.clone();
133        let hidden_columns = planning.projection.hidden_columns.clone();
134        let kernel_predicate = planning
135            .filters
136            .predicate
137            .as_ref()
138            .map(|predicate| {
139                delta_predicate_to_kernel_pruning(predicate).ok_or(
140                    DeltaReaderError::UnsupportedPredicate {
141                        reason: "datafusion_predicate_not_kernel_safe",
142                    },
143                )
144            })
145            .transpose()?;
146        let row_predicate = planning
147            .filters
148            .row_predicate
149            .as_ref()
150            .map(|predicate| {
151                delta_predicate_to_kernel_pruning(predicate).ok_or(
152                    DeltaReaderError::UnsupportedPredicate {
153                        reason: "exact_row_predicate_not_kernel_safe",
154                    },
155                )
156            })
157            .transpose()?;
158        let row_predicate = plan_row_predicate(
159            self.table.snapshot(),
160            physical_projection.as_deref(),
161            &hidden_columns,
162            row_predicate,
163        )?;
164        let core = plan_scan(
165            self.table.snapshot(),
166            physical_projection.as_deref(),
167            &hidden_columns,
168            kernel_predicate,
169            planning.filters.requires_statistics,
170            self.options.execution_options,
171            DeltaScanPartitionTargetOptions {
172                explicit_target_partitions: self.options.target_partitions,
173                caller_target_partitions: Some(state.config().target_partitions()),
174            },
175        )?;
176        let partition_count = core.partitions.len();
177        let plan = {
178            let _setup = tracing::debug_span!(
179                target: "delta_arrow_reader::profile",
180                "Delta scan execution setup"
181            )
182            .entered();
183            create_datafusion_execution_plan(
184                core,
185                planning,
186                row_predicate,
187                self.source_name.clone(),
188            )
189        };
190        Ok((plan, partition_count))
191    }
192}
193
194impl fmt::Debug for DeltaTableProvider {
195    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196        formatter
197            .debug_struct("DeltaTableProvider")
198            .field("snapshot_version", &self.table.version())
199            .finish_non_exhaustive()
200    }
201}
202
203#[async_trait]
204impl TableProvider for DeltaTableProvider {
205    fn schema(&self) -> SchemaRef {
206        Arc::clone(self.table.schema())
207    }
208
209    fn table_type(&self) -> TableType {
210        TableType::Base
211    }
212
213    async fn scan(
214        &self,
215        state: &dyn Session,
216        projection: Option<&Vec<usize>>,
217        filters: &[Expr],
218        _limit: Option<usize>,
219    ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
220        match self.plan(state, projection.map(Vec::as_slice), filters) {
221            Ok((plan, partition_count)) => {
222                tracing::debug!(
223                    target: TRACING_TARGET,
224                    event = "provider_scan.planned",
225                    snapshot_version = self.table.version(),
226                    partition_count,
227                    backend = ?self.options.execution_options.reader_backend(),
228                    outcome = "planned"
229                );
230                Ok(plan)
231            }
232            Err(error) => {
233                trace_failure(
234                    "provider_scan.failed",
235                    self.table.version(),
236                    self.options.execution_options.reader_backend(),
237                    &error,
238                );
239                Err(DataFusionError::External(Box::new(error)))
240            }
241        }
242    }
243
244    fn supports_filters_pushdown(
245        &self,
246        filters: &[&Expr],
247    ) -> DataFusionResult<Vec<TableProviderFilterPushDown>> {
248        let partition_columns = self
249            .table
250            .partition_columns()
251            .iter()
252            .cloned()
253            .collect::<HashSet<_>>();
254        let planning = plan_datafusion_filters(
255            self.table.schema(),
256            &partition_columns,
257            filters,
258            DataFusionFilterCapabilities {
259                exact_predicate_evaluation: self.options.execution_options.reader_backend()
260                    == DeltaReaderBackend::NativeAsync,
261            },
262        );
263        Ok(planning
264            .decisions
265            .iter()
266            .map(|decision| decision.pushdown.clone())
267            .collect())
268    }
269}
270
271/// Result of registering one loaded Delta table in a DataFusion context.
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct RegisteredDeltaTable {
274    /// Caller-supplied DataFusion table name.
275    pub name: String,
276    /// Loaded Delta snapshot version.
277    pub version: u64,
278}
279
280/// Registers one loaded Delta table in a DataFusion session.
281///
282/// Registration performs no scan. Existing registrations are preserved and
283/// reported through [`DeltaReaderError`].
284///
285/// ```no_run
286/// use datafusion::prelude::SessionContext;
287/// use delta_arrow_reader::{
288///     DeltaDataFusionScanOptions, DeltaTableBuilder, register_delta_table,
289/// };
290///
291/// # fn register() -> Result<(), Box<dyn std::error::Error>> {
292/// let context = SessionContext::new();
293/// let table = DeltaTableBuilder::new("/tmp/example-delta-table").load()?;
294/// let registered = register_delta_table(
295///     &context,
296///     "orders",
297///     table,
298///     DeltaDataFusionScanOptions::default(),
299/// )?;
300/// assert_eq!(registered.name, "orders");
301/// # Ok(())
302/// # }
303/// ```
304pub fn register_delta_table(
305    context: &SessionContext,
306    name: impl Into<String>,
307    table: DeltaTable,
308    options: DeltaDataFusionScanOptions,
309) -> Result<RegisteredDeltaTable, DeltaReaderError> {
310    let name = name.into();
311    let version = table.version();
312    let backend = options.execution_options.reader_backend();
313    let result = (|| {
314        validate_registration_name(&name)?;
315        let provider =
316            DeltaTableProvider::try_new_with_source_name(table, options, Some(name.clone()))?;
317        context
318            .register_table(name.as_str(), Arc::new(provider))
319            .map_err(|source| DeltaReaderError::DataFusionAdapter {
320                reason: "table_registration_failed",
321                source: Box::new(source),
322            })?;
323        Ok(RegisteredDeltaTable { name, version })
324    })();
325    match result {
326        Ok(registered) => {
327            tracing::debug!(
328                target: TRACING_TARGET,
329                event = "provider_registration.registered",
330                snapshot_version = version,
331                partition_count = tracing::field::Empty,
332                backend = ?backend,
333                outcome = "registered"
334            );
335            Ok(registered)
336        }
337        Err(error) => {
338            trace_failure("provider_registration.failed", version, backend, &error);
339            Err(error)
340        }
341    }
342}
343
344fn validate_registration_name(name: &str) -> Result<(), DeltaReaderError> {
345    let mut chars = name.chars();
346    let valid = chars
347        .next()
348        .is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
349        && chars.all(|value| value == '_' || value.is_ascii_alphanumeric());
350    if !valid || is_reserved_sql_keyword(name) {
351        let reason = if name.is_empty() {
352            "table_registration_name_empty"
353        } else {
354            "table_registration_name_invalid"
355        };
356        return Err(DeltaReaderError::DataFusionAdapter {
357            reason,
358            source: Box::new(DataFusionError::Plan(reason.to_owned())),
359        });
360    }
361    Ok(())
362}
363
364fn is_reserved_sql_keyword(name: &str) -> bool {
365    const KEYWORDS: &[&str] = &[
366        "all",
367        "alter",
368        "analyze",
369        "and",
370        "anti",
371        "as",
372        "asof",
373        "by",
374        "case",
375        "connect",
376        "cross",
377        "delete",
378        "distinct",
379        "distribute",
380        "drop",
381        "else",
382        "end",
383        "except",
384        "exists",
385        "explain",
386        "false",
387        "fetch",
388        "for",
389        "format",
390        "from",
391        "full",
392        "global",
393        "group",
394        "having",
395        "in",
396        "inner",
397        "insert",
398        "intersect",
399        "into",
400        "is",
401        "join",
402        "lateral",
403        "left",
404        "like",
405        "limit",
406        "minus",
407        "natural",
408        "not",
409        "null",
410        "offset",
411        "on",
412        "open",
413        "or",
414        "order",
415        "outer",
416        "partition",
417        "pivot",
418        "prewhere",
419        "qualify",
420        "returning",
421        "right",
422        "sample",
423        "select",
424        "semi",
425        "set",
426        "settings",
427        "sort",
428        "start",
429        "table",
430        "tablesample",
431        "then",
432        "top",
433        "true",
434        "union",
435        "unpivot",
436        "update",
437        "using",
438        "values",
439        "view",
440        "when",
441        "where",
442        "window",
443        "with",
444    ];
445    KEYWORDS
446        .iter()
447        .any(|keyword| name.eq_ignore_ascii_case(keyword))
448}
449
450fn trace_failure(
451    event: &'static str,
452    snapshot_version: u64,
453    backend: DeltaReaderBackend,
454    error: &DeltaReaderError,
455) {
456    tracing::debug!(
457        target: TRACING_TARGET,
458        event,
459        snapshot_version,
460        partition_count = tracing::field::Empty,
461        backend = ?backend,
462        outcome = "failed",
463        error_variant = error.as_str(),
464        error_phase = error.phase().as_str()
465    );
466}
467
468#[cfg(test)]
469mod tests {
470    use super::validate_registration_name;
471
472    #[test]
473    fn registration_names_preserve_the_frozen_unquoted_identifier_boundary() {
474        for name in ["orders", "_customers", "Regions_2026", "line_items"] {
475            assert!(validate_registration_name(name).is_ok(), "{name}");
476        }
477
478        for name in [
479            "",
480            "2026_orders",
481            "orders.latest",
482            "line-items",
483            "line items",
484            "\"orders\"",
485            "orders$",
486            "ordérs",
487            "select",
488            "FROM",
489            "Join",
490            "where",
491            "table",
492        ] {
493            assert!(validate_registration_name(name).is_err(), "{name}");
494        }
495    }
496}