Skip to content

feat(datafusion): Add opt-in eager file scan planning with output partitioning#2671

Open
toutane wants to merge 5 commits into
apache:mainfrom
toutane:datafusion-eager-file-scan-planning
Open

feat(datafusion): Add opt-in eager file scan planning with output partitioning#2671
toutane wants to merge 5 commits into
apache:mainfrom
toutane:datafusion-eager-file-scan-planning

Conversation

@toutane

@toutane toutane commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

What changes are included in this PR?

This PR adds an opt-in eager scan planning path for the DataFusion integration.

When iceberg.enable_eager_scan_planning is enabled, IcebergTableProvider::scan() plans FileScanTasks during physical planning, groups them across DataFusion target_partitions, and exposes the resulting output partition count as UnknownPartitioning(N). Each execute(partition) call then reads only the task group assigned to that output partition through ArrowReaderBuilder.

The default behavior is unchanged: eager planning is disabled by default, so scans keep the existing lazy single-partition planning path unless explicitly enabled.

This is a narrower split from #2298, focused on file-level eager planning and output partition count reporting. Some of the broader design discussions and review feedback happened in #2298; this PR keeps only the first scoped step and intentionally does not include hash partitioning, size-aware bin-packing, or row-group/sub-file planning.

Trade-offs:

  • Eager planning does catalog/metadata work during TableProvider::scan(), so it is kept opt-in
  • Task grouping is round-robin and count-based, not size-aware. This is simple and deterministic, but can be imbalanced when file sizes vary
  • Parallelism is at the FileScanTask level only. A table with one large file will not benefit from this change
  • The scan reports UnknownPartitioning(N), not hash partitioning. This exposes the number of output partitions without claiming stronger partitioning semantics

Follow-up work:

Are these changes tested?

Yes. Integration tests cover:

  • eager scan planning disabled by default
  • enabling eager scan planning through iceberg.enable_eager_scan_planning
  • exposing multiple scan output partitions when eager planning is enabled
  • preserving query results between single-partition and multi-partition scans

@toutane

toutane commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

👋 cc @mbutrovich @timsaucer

@mbutrovich mbutrovich self-requested a review July 9, 2026 15:05

@mbutrovich mbutrovich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, thanks @toutane!

@@ -146,13 +139,55 @@ impl ExecutionPlan for IcebergTableScan {
_partition: usize,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

execute's partition argument is read at line 146 (file_task_groups.get(_partition)) and interpolated into the error message at line 148 (partition {_partition} does not exist). The leading underscore signals "unused" to readers and lint, and it leaks into user-facing error text. Rename to partition. (_context at line 140 is still legitimately unused; leave it.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both IcebergTableProvider::scan (:158, used at :178) and IcebergStaticTableProvider::scan (:343, used at :352) pass _state into create_scan_plan, which reads it via enable_eager_scan_planning(state). Rename both to state. Note: _state at :365 (insert_into is genuinely unused; leave it.)

Comment on lines +123 to +131
let target_partitions = target_partitions.max(1);

let mut groups: Vec<Vec<FileScanTask>> = vec![Vec::new(); target_partitions];
for (i, task) in tasks.into_iter().enumerate() {
groups[i % target_partitions].push(task);
}

groups.retain(|group| !group.is_empty());
groups

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When target_partitions > tasks.len() (which the tests deliberately hit with data_file_count + 1), this allocates extra empty vecs and then walks the whole vec to remove them. The result is always min(target_partitions, tasks.len()) non-empty groups, so clamp up front and the retain disappears:

// tasks is non-empty here (early return above handles the empty case)
let target_partitions = target_partitions.max(1).min(tasks.len());

let mut groups: Vec<Vec<FileScanTask>> = vec![Vec::new(); target_partitions];
for (i, task) in tasks.into_iter().enumerate() {
    groups[i % target_partitions].push(task);
}

groups

Comment on lines +153 to +156
let task_count = file_task_group.len();
let tasks: FileScanTaskStream = Box::pin(futures::stream::iter(
(0..task_count).map(move |idx| Ok(file_task_group[idx].clone())),
));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

task_count is used once; inline it as (0..file_task_group.len()). Heads-up for anyone tempted to go further: file_task_group.iter().cloned().map(Ok) does not compile here, because the stream must be 'static and iter() would borrow the local Arc<[FileScanTask]>. The move-plus-index form is the allocation-free way to clone lazily out of the Arc, so keep that shape; just remove the extra binding.

@toutane

toutane commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @mbutrovich, addressed all four comments in d99603f

@toutane toutane force-pushed the datafusion-eager-file-scan-planning branch from ef774e8 to 6a4f1a1 Compare July 10, 2026 13:08
@toutane toutane force-pushed the datafusion-eager-file-scan-planning branch from 6a4f1a1 to be6d53e Compare July 10, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable parallel file-level scanning for IcebergTableScan Datafusion Integration

2 participants