@@ -28,6 +28,7 @@ use datafusion::physical_plan::execution_plan::{Boundedness, EmissionType};
2828use datafusion:: physical_plan:: stream:: RecordBatchStreamAdapter ;
2929use datafusion:: physical_plan:: { DisplayAs , ExecutionPlan , Partitioning , PlanProperties } ;
3030use datafusion:: prelude:: Expr ;
31+ use either:: Either ;
3132use futures:: { Stream , TryStreamExt } ;
3233use iceberg:: expr:: Predicate ;
3334use iceberg:: scan:: { FileScanTask , TableScan } ;
@@ -39,11 +40,9 @@ use crate::to_datafusion_error;
3940
4041/// Iceberg [`Table`] scan as a DataFusion [`ExecutionPlan`].
4142///
42- /// Has three construction modes: [`new`][Self::new] for a lazy
43- /// single-partition scan, [`new_with_tasks`][Self::new_with_tasks] for an
44- /// eager scan over pre-planned [`FileScanTask`] buckets, and
45- /// [`new_with_tasks_from_predicate`][Self::new_with_tasks_from_predicate] for
46- /// rebuilding an eager scan from a [`Predicate`].
43+ /// Prefer [`IcebergTableScanBuilder`] when composing optional scan inputs. The
44+ /// constructor helpers are retained for direct lazy scans and eager scans over
45+ /// pre-planned [`FileScanTask`] buckets.
4746///
4847/// Note: in eager mode the underlying `TableScan` is rebuilt on every
4948/// `execute(partition)` call. The per-build cost is bounded (no I/O) and
@@ -63,8 +62,8 @@ pub struct IcebergTableScan {
6362 table_schema : ArrowSchemaRef ,
6463 /// Projection indices as received by `scan`.
6564 projection_indices : Option < Vec < usize > > ,
66- /// Filters to apply to the table scan.
67- predicates : Option < Predicate > ,
65+ /// Predicate to apply to the table scan.
66+ predicate : Option < Predicate > ,
6867 /// Pre-planned file scan tasks per partition (eager mode), or `None` (lazy mode).
6968 buckets : Option < Vec < Vec < FileScanTask > > > ,
7069 /// `None` when partitioning is `UnknownPartitioning`.
@@ -73,7 +72,117 @@ pub struct IcebergTableScan {
7372 limit : Option < usize > ,
7473}
7574
75+ /// Builder for [`IcebergTableScan`].
76+ #[ derive( Debug ) ]
77+ pub struct IcebergTableScanBuilder {
78+ table : Table ,
79+ snapshot_id : Option < i64 > ,
80+ schema : ArrowSchemaRef ,
81+ projection : Option < Vec < usize > > ,
82+ filter_source : Option < Either < Vec < Expr > , Option < Predicate > > > ,
83+ limit : Option < usize > ,
84+ buckets : Option < Vec < Vec < FileScanTask > > > ,
85+ partitioning : Partitioning ,
86+ partition_keys_kind : Option < PartitionKeysKind > ,
87+ }
88+
89+ impl IcebergTableScanBuilder {
90+ pub fn new ( table : Table , schema : ArrowSchemaRef ) -> Self {
91+ Self {
92+ table,
93+ snapshot_id : None ,
94+ schema,
95+ projection : None ,
96+ filter_source : None ,
97+ limit : None ,
98+ buckets : None ,
99+ partitioning : Partitioning :: UnknownPartitioning ( 1 ) ,
100+ partition_keys_kind : None ,
101+ }
102+ }
103+
104+ pub fn with_snapshot_id ( mut self , snapshot_id : Option < i64 > ) -> Self {
105+ self . snapshot_id = snapshot_id;
106+ self
107+ }
108+
109+ pub fn with_projection ( mut self , projection : Option < & [ usize ] > ) -> Self {
110+ self . projection = projection. map ( <[ usize ] >:: to_vec) ;
111+ self
112+ }
113+
114+ /// Sets the filter source for the scan.
115+ ///
116+ /// Use `Either::Left(filters)` for DataFusion filters, or
117+ /// `Either::Right(predicate)` when rebuilding from an Iceberg predicate.
118+ pub fn with_filter_source ( mut self , filter_source : Either < & [ Expr ] , Option < Predicate > > ) -> Self {
119+ self . filter_source = Some ( match filter_source {
120+ Either :: Left ( filters) => Either :: Left ( filters. to_vec ( ) ) ,
121+ Either :: Right ( predicate) => Either :: Right ( predicate) ,
122+ } ) ;
123+ self
124+ }
125+
126+ pub fn with_limit ( mut self , limit : Option < usize > ) -> Self {
127+ self . limit = limit;
128+ self
129+ }
130+
131+ pub fn with_tasks (
132+ mut self ,
133+ buckets : Vec < Vec < FileScanTask > > ,
134+ partitioning : Partitioning ,
135+ ) -> Self {
136+ self . buckets = Some ( buckets) ;
137+ self . partitioning = partitioning;
138+ self
139+ }
140+
141+ pub ( crate ) fn with_partition_keys_kind (
142+ mut self ,
143+ partition_keys_kind : Option < PartitionKeysKind > ,
144+ ) -> Self {
145+ self . partition_keys_kind = partition_keys_kind;
146+ self
147+ }
148+
149+ pub fn build ( self ) -> IcebergTableScan {
150+ let Self {
151+ table,
152+ snapshot_id,
153+ schema,
154+ projection,
155+ filter_source,
156+ limit,
157+ buckets,
158+ partitioning,
159+ partition_keys_kind,
160+ } = self ;
161+ let predicate = match filter_source {
162+ Some ( Either :: Left ( filters) ) => convert_filters_to_predicate ( & filters) ,
163+ Some ( Either :: Right ( predicate) ) => predicate,
164+ None => None ,
165+ } ;
166+
167+ IcebergTableScan :: new_inner (
168+ table,
169+ snapshot_id,
170+ schema,
171+ projection. as_deref ( ) ,
172+ predicate,
173+ limit,
174+ partitioning,
175+ buckets,
176+ )
177+ . with_partition_keys_kind ( partition_keys_kind)
178+ }
179+ }
180+
76181impl IcebergTableScan {
182+ pub fn builder ( table : Table , schema : ArrowSchemaRef ) -> IcebergTableScanBuilder {
183+ IcebergTableScanBuilder :: new ( table, schema)
184+ }
185+
77186 /// Creates a lazy single-partition scan that plans and reads all tasks
78187 /// inside `execute(0)`. Used by
79188 /// [`IcebergStaticTableProvider`][crate::table::IcebergStaticTableProvider].
@@ -85,16 +194,12 @@ impl IcebergTableScan {
85194 filters : & [ Expr ] ,
86195 limit : Option < usize > ,
87196 ) -> Self {
88- Self :: new_inner (
89- table,
90- snapshot_id,
91- schema,
92- projection,
93- convert_filters_to_predicate ( filters) ,
94- limit,
95- Partitioning :: UnknownPartitioning ( 1 ) ,
96- None ,
97- )
197+ Self :: builder ( table, schema)
198+ . with_snapshot_id ( snapshot_id)
199+ . with_projection ( projection. map ( Vec :: as_slice) )
200+ . with_filter_source ( Either :: Left ( filters) )
201+ . with_limit ( limit)
202+ . build ( )
98203 }
99204
100205 /// Creates an eager multi-partition scan over pre-planned task buckets.
@@ -112,48 +217,21 @@ impl IcebergTableScan {
112217 buckets : Vec < Vec < FileScanTask > > ,
113218 partitioning : Partitioning ,
114219 ) -> Self {
115- Self :: new_inner (
116- table,
117- snapshot_id,
118- schema,
119- projection,
120- convert_filters_to_predicate ( filters) ,
121- limit,
122- partitioning,
123- Some ( buckets) ,
124- )
125- }
126-
127- /// Creates an eager multi-partition scan from a [`Predicate`].
128- #[ allow( clippy:: too_many_arguments) ]
129- pub fn new_with_tasks_from_predicate (
130- table : Table ,
131- snapshot_id : Option < i64 > ,
132- schema : ArrowSchemaRef ,
133- projection : Option < & Vec < usize > > ,
134- predicate : Option < Predicate > ,
135- limit : Option < usize > ,
136- buckets : Vec < Vec < FileScanTask > > ,
137- partitioning : Partitioning ,
138- ) -> Self {
139- Self :: new_inner (
140- table,
141- snapshot_id,
142- schema,
143- projection,
144- predicate,
145- limit,
146- partitioning,
147- Some ( buckets) ,
148- )
220+ Self :: builder ( table, schema)
221+ . with_snapshot_id ( snapshot_id)
222+ . with_projection ( projection. map ( Vec :: as_slice) )
223+ . with_filter_source ( Either :: Left ( filters) )
224+ . with_limit ( limit)
225+ . with_tasks ( buckets, partitioning)
226+ . build ( )
149227 }
150228
151229 #[ allow( clippy:: too_many_arguments) ]
152230 fn new_inner (
153231 table : Table ,
154232 snapshot_id : Option < i64 > ,
155233 schema : ArrowSchemaRef ,
156- projection : Option < & Vec < usize > > ,
234+ projection : Option < & [ usize ] > ,
157235 predicate : Option < Predicate > ,
158236 limit : Option < usize > ,
159237 partitioning : Partitioning ,
@@ -170,7 +248,7 @@ impl IcebergTableScan {
170248 Boundedness :: Bounded ,
171249 ) ) ;
172250 let table_schema = Arc :: clone ( & schema) ;
173- let projection_indices = projection. cloned ( ) ;
251+ let projection_indices = projection. map ( < [ usize ] > :: to_vec ) ;
174252 let projection = get_column_names ( schema, projection) ;
175253
176254 Self {
@@ -180,7 +258,7 @@ impl IcebergTableScan {
180258 projection,
181259 table_schema,
182260 projection_indices,
183- predicates : predicate,
261+ predicate,
184262 buckets,
185263 partition_keys_kind : None ,
186264 limit,
@@ -215,8 +293,8 @@ impl IcebergTableScan {
215293 self . projection_indices . as_deref ( )
216294 }
217295
218- pub fn predicates ( & self ) -> Option < & Predicate > {
219- self . predicates . as_ref ( )
296+ pub fn predicate ( & self ) -> Option < & Predicate > {
297+ self . predicate . as_ref ( )
220298 }
221299
222300 /// Returns the pre-planned file task buckets, or an empty slice in lazy mode.
@@ -290,7 +368,7 @@ impl ExecutionPlan for IcebergTableScan {
290368 self . table . clone ( ) ,
291369 self . snapshot_id ,
292370 self . projection . clone ( ) ,
293- self . predicates . clone ( ) ,
371+ self . predicate . clone ( ) ,
294372 bucket,
295373 ) ;
296374 let stream = Box :: pin ( futures:: stream:: once ( fut) . try_flatten ( ) )
@@ -319,7 +397,7 @@ impl DisplayAs for IcebergTableScan {
319397 . as_deref ( )
320398 . map_or ( String :: new ( ) , |v| v. join ( "," ) ) ;
321399 let predicate = self
322- . predicates
400+ . predicate
323401 . as_ref ( )
324402 . map_or ( String :: new ( ) , |p| p. to_string ( ) ) ;
325403
@@ -344,7 +422,7 @@ fn build_table_scan(
344422 table : Table ,
345423 snapshot_id : Option < i64 > ,
346424 column_names : Option < Vec < String > > ,
347- predicates : Option < Predicate > ,
425+ predicate : Option < Predicate > ,
348426) -> DFResult < TableScan > {
349427 let scan_builder = match snapshot_id {
350428 Some ( id) => table. scan ( ) . snapshot_id ( id) ,
@@ -354,7 +432,7 @@ fn build_table_scan(
354432 Some ( names) => scan_builder. select ( names) ,
355433 None => scan_builder. select_all ( ) ,
356434 } ;
357- if let Some ( pred) = predicates {
435+ if let Some ( pred) = predicate {
358436 scan_builder = scan_builder. with_filter ( pred) ;
359437 }
360438 scan_builder. build ( ) . map_err ( to_datafusion_error)
@@ -367,10 +445,10 @@ async fn build_record_batch_stream(
367445 table : Table ,
368446 snapshot_id : Option < i64 > ,
369447 column_names : Option < Vec < String > > ,
370- predicates : Option < Predicate > ,
448+ predicate : Option < Predicate > ,
371449 bucket : Option < Vec < FileScanTask > > ,
372450) -> DFResult < Pin < Box < dyn Stream < Item = DFResult < RecordBatch > > + Send > > > {
373- let table_scan = build_table_scan ( table, snapshot_id, column_names, predicates ) ?;
451+ let table_scan = build_table_scan ( table, snapshot_id, column_names, predicate ) ?;
374452 let stream: Pin < Box < dyn Stream < Item = DFResult < RecordBatch > > + Send > > = match bucket {
375453 Some ( bucket) => {
376454 let task_stream = Box :: pin ( futures:: stream:: iter (
@@ -416,7 +494,7 @@ fn apply_limit(
416494
417495pub ( super ) fn get_column_names (
418496 schema : ArrowSchemaRef ,
419- projection : Option < & Vec < usize > > ,
497+ projection : Option < & [ usize ] > ,
420498) -> Option < Vec < String > > {
421499 projection. map ( |v| {
422500 v. iter ( )
@@ -462,22 +540,19 @@ mod tests {
462540 }
463541
464542 #[ tokio:: test]
465- async fn test_predicate_constructor_exposes_rebuild_inputs ( ) {
543+ async fn test_builder_exposes_rebuild_inputs ( ) {
466544 let schema = create_test_arrow_schema ( ) ;
467545 let projection = vec ! [ 0usize , 2 ] ;
468546 let predicate = Reference :: new ( "x" ) . greater_than ( Datum :: long ( 5 ) ) ;
469- let scan = IcebergTableScan :: new_with_tasks_from_predicate (
470- get_test_table_from_metadata_file ( ) . await ,
471- None ,
472- schema. clone ( ) ,
473- Some ( & projection) ,
474- Some ( predicate. clone ( ) ) ,
475- Some ( 100 ) ,
476- vec ! [ vec![ ] , vec![ ] ] ,
477- Partitioning :: UnknownPartitioning ( 2 ) ,
478- ) ;
479-
480- assert_eq ! ( scan. predicates( ) , Some ( & predicate) ) ;
547+ let scan =
548+ IcebergTableScan :: builder ( get_test_table_from_metadata_file ( ) . await , schema. clone ( ) )
549+ . with_projection ( Some ( & projection) )
550+ . with_filter_source ( Either :: Right ( Some ( predicate. clone ( ) ) ) )
551+ . with_limit ( Some ( 100 ) )
552+ . with_tasks ( vec ! [ vec![ ] , vec![ ] ] , Partitioning :: UnknownPartitioning ( 2 ) )
553+ . build ( ) ;
554+
555+ assert_eq ! ( scan. predicate( ) , Some ( & predicate) ) ;
481556 assert_eq ! ( scan. table_schema( ) . fields( ) , schema. fields( ) ) ;
482557 assert_eq ! ( scan. table_schema( ) . fields( ) . len( ) , 3 ) ;
483558 assert_eq ! ( scan. schema( ) . fields( ) . len( ) , 2 ) ;
0 commit comments