Repository navigation
feat(scan): expose portable reader options for planned tasks - #3514
alexanderbianchi wants to merge 2 commits into
Conversation
| /// Contains no I/O handles, runtime, or planning state. Apply these options to a | ||
| /// locally constructed builder, then build one reader and clone it across | ||
| /// partitions to share its delete-file cache. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
There was a problem hiding this comment.
On the serialized-option evolution you asked about: every field is required on deserialize and the struct is Copy + Eq. A payload written by an older planner will fail with missing field on a newer worker once a sixth setting is added. A non-Copy field would also break the public API.
Consider #[serde(default)] (container level, with a Default that matches the ArrowReaderBuilder defaults) so new fields can be added later. Unknown fields from a newer planner are already ignored by serde.
Question (unverified): data_file_concurrency_limit defaults to available_parallelism() of the planning host and is now serialized. A coordinator with 64 cores would pass 64 to a 4-core worker unless the caller overrides it. For comparison, Java's SparkInputPartition ships caseSensitive, the expected schema, and cacheDeleteFilesOnExecutors, but no batch size or concurrency. Should this field be Option so an unset value keeps the local default, like batch_size?
|
|
||
| impl TableScanReaderOptions { | ||
| /// The requested record batch size, or the reader default. | ||
| pub fn batch_size(&self) -> Option<usize> { |
There was a problem hiding this comment.
The five getters have no caller in this PR and the description only needs apply_to plus serde. Each public getter is API to keep stable. Consider dropping them until a consumer needs one, which also shrinks public-api.txt.
| /// # Ok(()) | ||
| /// # } | ||
| /// ``` | ||
| pub fn reader_options(&self) -> TableScanReaderOptions { |
There was a problem hiding this comment.
The same five settings now live in TableScanBuilder, TableScan (two struct literals in build()), ArrowReaderBuilder, and this new type, and reader_options() copies them one by one. If it stays simple, store a TableScanReaderOptions in TableScanBuilder and TableScan with the with_* setters writing into it. Then reader_options() returns self.reader_options and both TableScan { .. } literals lose five fields each. Skip if the setter churn outweighs that.
There was a problem hiding this comment.
No, we should not add this method, and we should remove all methods used to config arrow reader options.
| async fn reader_options_preserve_reads_after_dropping_scan() { | ||
| let mut fixture = TableTestFixture::new(); | ||
| fixture.setup_multi_row_group_manifest(&[150, 299]).await; | ||
| for enabled in [false, true] { |
There was a problem hiding this comment.
The for enabled in [false, true] loop does not add coverage for the flags. Mutation check: deleting the row_selection_enabled and bloom_filter_enabled assignments in apply_to fails only applies_scan_settings_and_preserves_local_io_tuning. This test still passes because the result equals to_arrow() either way. Consider a single iteration and keep what only this test covers: the JSON shape, drop-scan, fresh local I/O, and the batch size bound.
blackmwk
left a comment
There was a problem hiding this comment.
Thanks @alexanderbianchi for this pr, I have some opnions about the api design
| /// locally constructed builder, then build one reader and clone it across | ||
| /// partitions to share its delete-file cache. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub struct TableScanReaderOptions { |
There was a problem hiding this comment.
We should derive typed buidler rather than make field crate visible, all fields should be private.
There was a problem hiding this comment.
https://github.com/alexanderbianchi/iceberg-rust/blob/e9d2a8a99399ab0440402c5935e37f797cbae60d/crates/iceberg/src/arrow/reader/options.rs there is an options.rs already. Should we combine them? Move options.rs to parquet_options.rs?
| /// locally constructed builder, then build one reader and clone it across | ||
| /// partitions to share its delete-file cache. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub struct TableScanReaderOptions { |
There was a problem hiding this comment.
| pub struct TableScanReaderOptions { | |
| pub struct ArrowReaderOptions { |
| @@ -602,23 +602,51 @@ impl TableScan { | |||
|
|
|||
| /// Returns an [`ArrowRecordBatchStream`]. | |||
| pub async fn to_arrow(&self) -> Result<ArrowRecordBatchStream> { | |||
There was a problem hiding this comment.
| pub async fn to_arrow(&self) -> Result<ArrowRecordBatchStream> { | |
| pub async fn to_arrow(&self, options: ArrowReaderOptions) -> Result<ArrowRecordBatchStream> { |
| /// # Ok(()) | ||
| /// # } | ||
| /// ``` | ||
| pub fn reader_options(&self) -> TableScanReaderOptions { |
There was a problem hiding this comment.
No, we should not add this method, and we should remove all methods used to config arrow reader options.
|
|
||
| /// Applies all scan reader settings, preserving the builder's local resources | ||
| /// and Parquet I/O tuning. An unset batch size restores the reader default. | ||
| pub fn apply_to(&self, mut builder: ArrowReaderBuilder) -> ArrowReaderBuilder { |
There was a problem hiding this comment.
We should not add this, instead, we should add a with_options methods in ArrowReaderBuidler and pass in this struct.
Which issue does this PR close?
Follows up on the reader-options discussion in #2671 and supports apache/datafusion-iceberg#67. Does not close #2671; the DataFusion integration continues in its own repository.
What changes are included in this PR?
Separate reader settings from file planning with a serializable
ArrowReaderOptionsvalue.TableScan::to_arrow(options)andArrowReaderBuilder::with_options(options)use the same mapping, letting eager consumers retain tasks and settings, discard the scan, and construct readers with local I/O and runtime resources.Options cover batch size, data-file concurrency, row-group filtering, row selection, and bloom filtering. Unset concurrency resolves on the execution host when the reader is built. Fields are private, with a typed builder and serde defaults for missing fields. Local Parquet I/O tuning survives applying the options. Clone one reader across partitions to share delete loading.
API migration: move reader-setting calls from
TableScanBuildertoArrowReaderOptions::builder(), then pass the result toto_arrow(options). For defaults, useto_arrow(Default::default()). Scanwith_concurrency_limit()now controls manifest planning only; configurewith_data_file_concurrency_limit(Some(limit))on reader options to also limit data reads.This enables #67 to preserve non-default reader settings in a follow-up, without adding an eager planner, a DataFusion dependency, or changes to the task wire format.
Are these changes tested?
Regression coverage serializes settings, drops the scan, and reads filtered tasks with position deletes using fresh local I/O/runtime. It compares results with
to_arrow(options)and checks batch sizing. Focused tests cover default decoding, unknown fields, explicit settings, execution-host defaults, and preservation of Parquet I/O tuning.Passed: 2,032 library tests (one existing ignored test), 87 documentation tests, Clippy with all targets/features and
-D warnings, formatting, andgit diff --check. Compiled all targets in the affected examples, S3 Tables catalog, and integration-test crates; external-service integration tests were not executed. Regenerated and inspectediceberg/public-api.txt.AI Disclosure
Implemented with Codex assistance. Review focus: migration from scan configuration to reader options, serialized-option evolution, and resolving unset concurrency on the execution host.