1use std::{collections::HashSet, fmt, sync::Arc};
4
5use arrow::datatypes::{DataType, Schema, SchemaRef};
6use async_trait::async_trait;
7use datafusion::{
8 catalog::Session,
9 common::{DataFusionError, Result as DataFusionResult},
10 datasource::{TableProvider, TableType, physical_plan::wrap_partition_type_in_dict},
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 transform::schema_with_view_types,
27};
28
29const TRACING_TARGET: &str = "delta_arrow_reader::datafusion";
30
31#[derive(Debug, Clone)]
33pub struct DeltaDataFusionScanOptions {
34 pub execution_options: DeltaReaderExecutionOptions,
36 pub target_partitions: Option<usize>,
38 pub use_view_types: bool,
40}
41
42impl Default for DeltaDataFusionScanOptions {
43 fn default() -> Self {
44 Self {
45 execution_options: DeltaReaderExecutionOptions::default(),
46 target_partitions: None,
47 use_view_types: true,
48 }
49 }
50}
51
52#[derive(Clone)]
72pub struct DeltaTableProvider {
73 table: DeltaTable,
74 schema: SchemaRef,
75 options: DeltaDataFusionScanOptions,
76 source_name: Option<String>,
77}
78
79impl DeltaTableProvider {
80 pub fn try_new(
82 table: DeltaTable,
83 options: DeltaDataFusionScanOptions,
84 ) -> Result<Self, DeltaReaderError> {
85 Self::try_new_with_source_name(table, options, None)
86 }
87
88 fn try_new_with_source_name(
89 table: DeltaTable,
90 options: DeltaDataFusionScanOptions,
91 source_name: Option<String>,
92 ) -> Result<Self, DeltaReaderError> {
93 options.execution_options.validate()?;
94 validate_backend_available(options.execution_options)?;
95 if options.target_partitions == Some(0) {
96 return Err(DeltaReaderError::InvalidConfiguration {
97 reason: "scan_partition_target_must_be_positive",
98 });
99 }
100 table.validate_protocol()?;
101 let partition_columns = table.partition_columns().iter().cloned().collect();
102 let schema = datafusion_schema(table.schema(), &partition_columns, options.use_view_types);
103 Ok(Self {
104 table,
105 schema,
106 options,
107 source_name,
108 })
109 }
110
111 fn plan(
112 &self,
113 state: &dyn Session,
114 projection: Option<&[usize]>,
115 filters: &[Expr],
116 ) -> Result<(Arc<dyn ExecutionPlan>, usize), DeltaReaderError> {
117 let _planning = tracing::debug_span!(
118 target: "delta_arrow_reader::profile",
119 "Delta scan planning"
120 )
121 .entered();
122 let partition_columns = self
123 .table
124 .partition_columns()
125 .iter()
126 .cloned()
127 .collect::<HashSet<_>>();
128 let filter_refs = filters.iter().collect::<Vec<_>>();
129 let mut planning = plan_datafusion_scan(
130 self.table.schema(),
131 &partition_columns,
132 projection,
133 &filter_refs,
134 DataFusionFilterCapabilities {
135 exact_predicate_evaluation: self.options.execution_options.reader_backend()
136 == DeltaReaderBackend::NativeAsync,
137 },
138 )?;
139 if planning
140 .filters
141 .decisions
142 .iter()
143 .any(|decision| decision.pushdown == TableProviderFilterPushDown::Unsupported)
144 {
145 return Err(DeltaReaderError::UnsupportedPredicate {
146 reason: "datafusion_scan_contains_unsupported_filter",
147 });
148 }
149 let physical_projection = planning.projection.physical_projection.clone();
150 let hidden_columns = planning.projection.hidden_columns.clone();
151 let kernel_predicate = planning
152 .filters
153 .predicate
154 .as_ref()
155 .map(|predicate| {
156 delta_predicate_to_kernel_pruning(predicate).ok_or(
157 DeltaReaderError::UnsupportedPredicate {
158 reason: "datafusion_predicate_not_kernel_safe",
159 },
160 )
161 })
162 .transpose()?;
163 let row_predicate = planning
164 .filters
165 .row_predicate
166 .as_ref()
167 .map(|predicate| {
168 delta_predicate_to_kernel_pruning(predicate).ok_or(
169 DeltaReaderError::UnsupportedPredicate {
170 reason: "exact_row_predicate_not_kernel_safe",
171 },
172 )
173 })
174 .transpose()?;
175 let row_predicate = plan_row_predicate(
176 self.table.snapshot(),
177 physical_projection.as_deref(),
178 &hidden_columns,
179 row_predicate,
180 )?;
181 let mut core = plan_scan(
182 self.table.snapshot(),
183 physical_projection.as_deref(),
184 &hidden_columns,
185 kernel_predicate,
186 planning.filters.requires_statistics,
187 self.options.execution_options,
188 DeltaScanPartitionTargetOptions {
189 explicit_target_partitions: self.options.target_partitions,
190 caller_target_partitions: Some(state.config().target_partitions()),
191 },
192 )?;
193 core.logical_schema = datafusion_schema(
194 &core.logical_schema,
195 &partition_columns,
196 self.options.use_view_types,
197 );
198 core.physical_schema = datafusion_schema(
199 &core.physical_schema,
200 &partition_columns,
201 self.options.use_view_types,
202 );
203 core.projected_schema = datafusion_schema(
204 &core.projected_schema,
205 &partition_columns,
206 self.options.use_view_types,
207 );
208 planning.projection.output_schema = datafusion_schema(
209 &planning.projection.output_schema,
210 &partition_columns,
211 self.options.use_view_types,
212 );
213 let partition_count = core.partitions.len();
214 let plan = {
215 let _setup = tracing::debug_span!(
216 target: "delta_arrow_reader::profile",
217 "Delta scan execution setup"
218 )
219 .entered();
220 create_datafusion_execution_plan(
221 core,
222 planning,
223 row_predicate,
224 self.source_name.clone(),
225 self.options.use_view_types,
226 )
227 };
228 Ok((plan, partition_count))
229 }
230}
231
232fn datafusion_schema(
233 schema: &Schema,
234 partition_columns: &HashSet<String>,
235 use_view_types: bool,
236) -> SchemaRef {
237 let view_schema = schema_with_view_types(schema);
238 Arc::new(Schema::new_with_metadata(
239 schema
240 .fields()
241 .iter()
242 .zip(view_schema.fields())
243 .map(|(logical, view)| {
244 if partition_columns.contains(logical.name())
245 && matches!(
246 logical.data_type(),
247 DataType::Utf8
248 | DataType::LargeUtf8
249 | DataType::Binary
250 | DataType::LargeBinary
251 )
252 {
253 Arc::new(
254 logical
255 .as_ref()
256 .clone()
257 .with_data_type(wrap_partition_type_in_dict(
258 logical.data_type().clone(),
259 )),
260 )
261 } else if use_view_types {
262 Arc::clone(view)
263 } else {
264 Arc::clone(logical)
265 }
266 })
267 .collect::<Vec<_>>(),
268 schema.metadata().clone(),
269 ))
270}
271
272impl fmt::Debug for DeltaTableProvider {
273 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274 formatter
275 .debug_struct("DeltaTableProvider")
276 .field("snapshot_version", &self.table.version())
277 .finish_non_exhaustive()
278 }
279}
280
281#[async_trait]
282impl TableProvider for DeltaTableProvider {
283 fn schema(&self) -> SchemaRef {
284 Arc::clone(&self.schema)
285 }
286
287 fn table_type(&self) -> TableType {
288 TableType::Base
289 }
290
291 async fn scan(
292 &self,
293 state: &dyn Session,
294 projection: Option<&Vec<usize>>,
295 filters: &[Expr],
296 _limit: Option<usize>,
297 ) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
298 match self.plan(state, projection.map(Vec::as_slice), filters) {
299 Ok((plan, partition_count)) => {
300 tracing::debug!(
301 target: TRACING_TARGET,
302 event = "provider_scan.planned",
303 snapshot_version = self.table.version(),
304 partition_count,
305 backend = ?self.options.execution_options.reader_backend(),
306 outcome = "planned"
307 );
308 Ok(plan)
309 }
310 Err(error) => {
311 trace_failure(
312 "provider_scan.failed",
313 self.table.version(),
314 self.options.execution_options.reader_backend(),
315 &error,
316 );
317 Err(DataFusionError::External(Box::new(error)))
318 }
319 }
320 }
321
322 fn supports_filters_pushdown(
323 &self,
324 filters: &[&Expr],
325 ) -> DataFusionResult<Vec<TableProviderFilterPushDown>> {
326 let partition_columns = self
327 .table
328 .partition_columns()
329 .iter()
330 .cloned()
331 .collect::<HashSet<_>>();
332 let planning = plan_datafusion_filters(
333 self.table.schema(),
334 &partition_columns,
335 filters,
336 DataFusionFilterCapabilities {
337 exact_predicate_evaluation: self.options.execution_options.reader_backend()
338 == DeltaReaderBackend::NativeAsync,
339 },
340 );
341 Ok(planning
342 .decisions
343 .iter()
344 .map(|decision| decision.pushdown.clone())
345 .collect())
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct RegisteredDeltaTable {
352 pub name: String,
354 pub version: u64,
356}
357
358pub fn register_delta_table(
383 context: &SessionContext,
384 name: impl Into<String>,
385 table: DeltaTable,
386 options: DeltaDataFusionScanOptions,
387) -> Result<RegisteredDeltaTable, DeltaReaderError> {
388 let name = name.into();
389 let version = table.version();
390 let backend = options.execution_options.reader_backend();
391 let result = (|| {
392 validate_registration_name(&name)?;
393 let provider =
394 DeltaTableProvider::try_new_with_source_name(table, options, Some(name.clone()))?;
395 context
396 .register_table(name.as_str(), Arc::new(provider))
397 .map_err(|source| DeltaReaderError::DataFusionAdapter {
398 reason: "table_registration_failed",
399 source: Box::new(source),
400 })?;
401 Ok(RegisteredDeltaTable { name, version })
402 })();
403 match result {
404 Ok(registered) => {
405 tracing::debug!(
406 target: TRACING_TARGET,
407 event = "provider_registration.registered",
408 snapshot_version = version,
409 partition_count = tracing::field::Empty,
410 backend = ?backend,
411 outcome = "registered"
412 );
413 Ok(registered)
414 }
415 Err(error) => {
416 trace_failure("provider_registration.failed", version, backend, &error);
417 Err(error)
418 }
419 }
420}
421
422fn validate_registration_name(name: &str) -> Result<(), DeltaReaderError> {
423 let mut chars = name.chars();
424 let valid = chars
425 .next()
426 .is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
427 && chars.all(|value| value == '_' || value.is_ascii_alphanumeric());
428 if !valid || is_reserved_sql_keyword(name) {
429 let reason = if name.is_empty() {
430 "table_registration_name_empty"
431 } else {
432 "table_registration_name_invalid"
433 };
434 return Err(DeltaReaderError::DataFusionAdapter {
435 reason,
436 source: Box::new(DataFusionError::Plan(reason.to_owned())),
437 });
438 }
439 Ok(())
440}
441
442fn is_reserved_sql_keyword(name: &str) -> bool {
443 const KEYWORDS: &[&str] = &[
444 "all",
445 "alter",
446 "analyze",
447 "and",
448 "anti",
449 "as",
450 "asof",
451 "by",
452 "case",
453 "connect",
454 "cross",
455 "delete",
456 "distinct",
457 "distribute",
458 "drop",
459 "else",
460 "end",
461 "except",
462 "exists",
463 "explain",
464 "false",
465 "fetch",
466 "for",
467 "format",
468 "from",
469 "full",
470 "global",
471 "group",
472 "having",
473 "in",
474 "inner",
475 "insert",
476 "intersect",
477 "into",
478 "is",
479 "join",
480 "lateral",
481 "left",
482 "like",
483 "limit",
484 "minus",
485 "natural",
486 "not",
487 "null",
488 "offset",
489 "on",
490 "open",
491 "or",
492 "order",
493 "outer",
494 "partition",
495 "pivot",
496 "prewhere",
497 "qualify",
498 "returning",
499 "right",
500 "sample",
501 "select",
502 "semi",
503 "set",
504 "settings",
505 "sort",
506 "start",
507 "table",
508 "tablesample",
509 "then",
510 "top",
511 "true",
512 "union",
513 "unpivot",
514 "update",
515 "using",
516 "values",
517 "view",
518 "when",
519 "where",
520 "window",
521 "with",
522 ];
523 KEYWORDS
524 .iter()
525 .any(|keyword| name.eq_ignore_ascii_case(keyword))
526}
527
528fn trace_failure(
529 event: &'static str,
530 snapshot_version: u64,
531 backend: DeltaReaderBackend,
532 error: &DeltaReaderError,
533) {
534 tracing::debug!(
535 target: TRACING_TARGET,
536 event,
537 snapshot_version,
538 partition_count = tracing::field::Empty,
539 backend = ?backend,
540 outcome = "failed",
541 error_variant = error.as_str(),
542 error_phase = error.phase().as_str()
543 );
544}
545
546#[cfg(test)]
547mod tests {
548 use std::collections::{HashMap, HashSet};
549
550 use arrow::datatypes::{DataType, Field, Schema};
551
552 use super::{datafusion_schema, validate_registration_name};
553
554 #[test]
555 fn registration_names_preserve_the_frozen_unquoted_identifier_boundary() {
556 for name in ["orders", "_customers", "Regions_2026", "line_items"] {
557 assert!(validate_registration_name(name).is_ok(), "{name}");
558 }
559
560 for name in [
561 "",
562 "2026_orders",
563 "orders.latest",
564 "line-items",
565 "line items",
566 "\"orders\"",
567 "orders$",
568 "ordérs",
569 "select",
570 "FROM",
571 "Join",
572 "where",
573 "table",
574 ] {
575 assert!(validate_registration_name(name).is_err(), "{name}");
576 }
577 }
578
579 #[test]
580 fn datafusion_schema_uses_views_except_for_dictionary_partitions() {
581 let field_metadata = HashMap::from([("field-key".to_owned(), "field-value".to_owned())]);
582 let schema_metadata = HashMap::from([("schema-key".to_owned(), "schema-value".to_owned())]);
583 let schema = Schema::new_with_metadata(
584 vec![
585 Field::new("text", DataType::Utf8, true).with_metadata(field_metadata.clone()),
586 Field::new("payload", DataType::Binary, true),
587 Field::new("region", DataType::Utf8, true),
588 Field::new("partition_payload", DataType::LargeBinary, true),
589 Field::new("id", DataType::Int32, false),
590 ],
591 schema_metadata.clone(),
592 );
593 let partitions = HashSet::from(["region".to_owned(), "partition_payload".to_owned()]);
594
595 let mapped = datafusion_schema(&schema, &partitions, true);
596
597 assert_eq!(
598 mapped.as_ref(),
599 &Schema::new_with_metadata(
600 vec![
601 Field::new("text", DataType::Utf8View, true).with_metadata(field_metadata),
602 Field::new("payload", DataType::BinaryView, true),
603 Field::new(
604 "region",
605 DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8)),
606 true,
607 ),
608 Field::new(
609 "partition_payload",
610 DataType::Dictionary(
611 Box::new(DataType::UInt16),
612 Box::new(DataType::LargeBinary),
613 ),
614 true,
615 ),
616 Field::new("id", DataType::Int32, false),
617 ],
618 schema_metadata.clone(),
619 )
620 );
621
622 let standard = datafusion_schema(&schema, &partitions, false);
623 assert_eq!(standard.field(0).data_type(), &DataType::Utf8);
624 assert_eq!(standard.field(1).data_type(), &DataType::Binary);
625 assert_eq!(
626 standard.field(2).data_type(),
627 &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::Utf8))
628 );
629 assert_eq!(
630 standard.field(3).data_type(),
631 &DataType::Dictionary(Box::new(DataType::UInt16), Box::new(DataType::LargeBinary),)
632 );
633 assert_eq!(standard.field(4).data_type(), &DataType::Int32);
634 assert_eq!(standard.metadata(), &schema_metadata);
635 }
636}