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