1use 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#[derive(Debug, Clone, Default)]
32pub struct DeltaDataFusionScanOptions {
33 pub execution_options: DeltaReaderExecutionOptions,
35 pub target_partitions: Option<usize>,
37}
38
39#[derive(Clone)]
59pub struct DeltaTableProvider {
60 table: DeltaTable,
61 options: DeltaDataFusionScanOptions,
62 source_name: Option<String>,
63}
64
65impl DeltaTableProvider {
66 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#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct RegisteredDeltaTable {
274 pub name: String,
276 pub version: u64,
278}
279
280pub 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}