@@ -47,7 +47,7 @@ pub enum IncrementalScanMode {
4747#[ derive( Debug , Clone ) ]
4848pub enum IncrementalSplit {
4949 Data ( DataSplit ) ,
50- /// Per-(partition, bucket) diff pair. Memory bounded by one bucket's data.
50+ /// Per-(partition, bucket) diff pair.
5151 DiffPair {
5252 before : Vec < DataSplit > ,
5353 after : Vec < DataSplit > ,
@@ -66,6 +66,80 @@ impl IncrementalPlan {
6666 Self { mode, splits }
6767 }
6868
69+ pub fn try_new (
70+ mode : IncrementalScanMode ,
71+ splits : Vec < IncrementalSplit > ,
72+ ) -> crate :: Result < Self > {
73+ let plan = Self :: new ( mode, splits) ;
74+ plan. validate ( ) ?;
75+ Ok ( plan)
76+ }
77+
78+ /// Validate the plan at every point it crosses into a reader.
79+ ///
80+ /// `new` is retained for source compatibility, so callers can still build
81+ /// an invalid plan. Readers must call this method instead of assuming a
82+ /// plan came from the scanner.
83+ pub fn validate ( & self ) -> crate :: Result < ( ) > {
84+ if self . mode == IncrementalScanMode :: Auto {
85+ return Err ( crate :: Error :: DataInvalid {
86+ message : "Incremental plan mode Auto must be resolved before consumption"
87+ . to_string ( ) ,
88+ source : None ,
89+ } ) ;
90+ }
91+ if self . mode == IncrementalScanMode :: Diff {
92+ let mut before_snapshot_id = None ;
93+ let mut after_snapshot_id = None ;
94+ for split in & self . splits {
95+ let IncrementalSplit :: DiffPair { before, after } = split else {
96+ return Err ( crate :: Error :: DataInvalid {
97+ message : "Diff incremental plan contains a Data split" . to_string ( ) ,
98+ source : None ,
99+ } ) ;
100+ } ;
101+ validate_diff_pair ( before, after) ?;
102+ if let Some ( snapshot_id) = before. first ( ) . map ( DataSplit :: snapshot_id) {
103+ if before_snapshot_id. is_some_and ( |expected| expected != snapshot_id) {
104+ return Err ( crate :: Error :: DataInvalid {
105+ message : "Diff plan contains different before snapshots" . to_string ( ) ,
106+ source : None ,
107+ } ) ;
108+ }
109+ before_snapshot_id = Some ( snapshot_id) ;
110+ }
111+ if let Some ( snapshot_id) = after. first ( ) . map ( DataSplit :: snapshot_id) {
112+ if after_snapshot_id. is_some_and ( |expected| expected != snapshot_id) {
113+ return Err ( crate :: Error :: DataInvalid {
114+ message : "Diff plan contains different after snapshots" . to_string ( ) ,
115+ source : None ,
116+ } ) ;
117+ }
118+ after_snapshot_id = Some ( snapshot_id) ;
119+ }
120+ }
121+ if let ( Some ( before) , Some ( after) ) = ( before_snapshot_id, after_snapshot_id) {
122+ if before >= after {
123+ return Err ( crate :: Error :: DataInvalid {
124+ message : "Diff plan before snapshot must be earlier than after snapshot"
125+ . to_string ( ) ,
126+ source : None ,
127+ } ) ;
128+ }
129+ }
130+ } else if self
131+ . splits
132+ . iter ( )
133+ . any ( |split| matches ! ( split, IncrementalSplit :: DiffPair { .. } ) )
134+ {
135+ return Err ( crate :: Error :: DataInvalid {
136+ message : "Non-Diff incremental plan contains a DiffPair" . to_string ( ) ,
137+ source : None ,
138+ } ) ;
139+ }
140+ Ok ( ( ) )
141+ }
142+
69143 /// Resolved mode (`Auto` already collapsed to `Delta` / `Changelog`).
70144 pub fn mode ( & self ) -> IncrementalScanMode {
71145 self . mode
@@ -86,6 +160,49 @@ impl IncrementalPlan {
86160 }
87161}
88162
163+ pub ( crate ) fn validate_diff_pair ( before : & [ DataSplit ] , after : & [ DataSplit ] ) -> crate :: Result < ( ) > {
164+ if before
165+ . iter ( )
166+ . chain ( after)
167+ . any ( |split| split. row_ranges ( ) . is_some ( ) )
168+ {
169+ return Err ( crate :: Error :: DataInvalid {
170+ message : "Diff pair must not contain physical row ranges" . to_string ( ) ,
171+ source : None ,
172+ } ) ;
173+ }
174+ let first = before. first ( ) . or ( after. first ( ) ) ;
175+ let Some ( first) = first else {
176+ return Ok ( ( ) ) ;
177+ } ;
178+ for side in [ before, after] {
179+ if let Some ( first_in_side) = side. first ( ) {
180+ if side
181+ . iter ( )
182+ . any ( |split| split. snapshot_id ( ) != first_in_side. snapshot_id ( ) )
183+ {
184+ return Err ( crate :: Error :: DataInvalid {
185+ message : "Diff pair side contains splits from different snapshots" . to_string ( ) ,
186+ source : None ,
187+ } ) ;
188+ }
189+ }
190+ }
191+ for split in before. iter ( ) . chain ( after) {
192+ if split. partition ( ) != first. partition ( )
193+ || split. bucket ( ) != first. bucket ( )
194+ || split. bucket_path ( ) != first. bucket_path ( )
195+ || split. total_buckets ( ) != first. total_buckets ( )
196+ {
197+ return Err ( crate :: Error :: DataInvalid {
198+ message : "Diff pair contains splits from different partition buckets" . to_string ( ) ,
199+ source : None ,
200+ } ) ;
201+ }
202+ }
203+ Ok ( ( ) )
204+ }
205+
89206/// Batch incremental scan over a snapshot id range.
90207pub struct IncrementalScan < ' a > {
91208 table : & ' a Table ,
@@ -202,7 +319,7 @@ impl<'a> IncrementalScan<'a> {
202319 let plan = self . scan . plan_snapshot_delta ( & snapshot) . await ?;
203320 splits. extend ( plan. splits ( ) . iter ( ) . cloned ( ) . map ( IncrementalSplit :: Data ) ) ;
204321 }
205- Ok ( IncrementalPlan :: new ( mode, splits) )
322+ IncrementalPlan :: try_new ( mode, splits)
206323 }
207324
208325 async fn plan_changelog ( & self , mode : IncrementalScanMode ) -> crate :: Result < IncrementalPlan > {
@@ -220,10 +337,15 @@ impl<'a> IncrementalScan<'a> {
220337 let plan = self . scan . plan_snapshot_changelog ( & snapshot) . await ?;
221338 splits. extend ( plan. splits ( ) . iter ( ) . cloned ( ) . map ( IncrementalSplit :: Data ) ) ;
222339 }
223- Ok ( IncrementalPlan :: new ( mode, splits) )
340+ IncrementalPlan :: try_new ( mode, splits)
224341 }
225342
226343 async fn plan_diff ( & self , mode : IncrementalScanMode ) -> crate :: Result < IncrementalPlan > {
344+ if self . table . schema ( ) . primary_keys ( ) . is_empty ( ) {
345+ return Err ( crate :: Error :: Unsupported {
346+ message : "Batch incremental Diff requires a table with primary keys" . to_string ( ) ,
347+ } ) ;
348+ }
227349 let core_options = CoreOptions :: new ( self . table . schema ( ) . options ( ) ) ;
228350 if core_options. merge_engine ( ) ? != crate :: spec:: MergeEngine :: Deduplicate {
229351 return Err ( crate :: Error :: Unsupported {
@@ -269,6 +391,6 @@ impl<'a> IncrementalScan<'a> {
269391 splits. push ( IncrementalSplit :: DiffPair { before, after } ) ;
270392 }
271393
272- Ok ( IncrementalPlan :: new ( mode, splits) )
394+ IncrementalPlan :: try_new ( mode, splits)
273395 }
274396}
0 commit comments