Skip to content

Commit 85c0a2b

Browse files
committed
fix: correct batch incremental diff reads
1 parent 10fe992 commit 85c0a2b

7 files changed

Lines changed: 975 additions & 163 deletions

File tree

crates/paimon/src/table/audit_log_table.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ impl AuditLogTable {
8181
}
8282

8383
pub fn to_arrow(&self, plan: &IncrementalPlan) -> crate::Result<ArrowRecordBatchStream> {
84+
plan.validate()?;
8485
let read = self.wrapped.new_read_builder().new_read()?;
8586
read.to_audit_log_arrow(plan)
8687
}

crates/paimon/src/table/incremental_scan.rs

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub enum IncrementalScanMode {
4747
#[derive(Debug, Clone)]
4848
pub 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.
90207
pub 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
}

crates/paimon/src/table/kv_file_reader.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ pub(crate) struct KeyValueReadConfig {
7070
pub primary_keys: Vec<String>,
7171
pub merge_engine: MergeEngine,
7272
pub sequence_fields: Vec<String>,
73+
/// Merge files from all supplied splits into one globally key-sorted stream.
74+
pub merge_splits: bool,
7375
}
7476

7577
/// Keep only the conjuncts of `predicates` that reference primary-key columns,
@@ -301,7 +303,15 @@ impl KeyValueFileReader {
301303
}
302304
}
303305

304-
let splits: Vec<DataSplit> = data_splits.to_vec();
306+
let split_groups: Vec<Vec<DataSplit>> = if self.config.merge_splits {
307+
vec![data_splits.to_vec()]
308+
} else {
309+
data_splits
310+
.iter()
311+
.cloned()
312+
.map(|split| vec![split])
313+
.collect()
314+
};
305315
let file_io = self.file_io;
306316
let merge_engine = self.config.merge_engine;
307317
let schema_manager = self.config.schema_manager;
@@ -321,21 +331,23 @@ impl KeyValueFileReader {
321331
let merge_output_schema = build_target_arrow_schema(&merge_output_fields)?;
322332

323333
Ok(try_stream! {
324-
for split in &splits {
334+
for split_group in &split_groups {
325335
// DV mode should not reach KeyValueFileReader.
326-
if split
327-
.data_deletion_files()
328-
.is_some_and(|files| files.iter().any(Option::is_some))
329-
{
330-
Err(Error::Unsupported {
331-
message: "KeyValueFileReader does not support deletion vectors".to_string(),
332-
})?;
336+
for split in split_group {
337+
if split
338+
.data_deletion_files()
339+
.is_some_and(|files| files.iter().any(Option::is_some))
340+
{
341+
Err(Error::Unsupported {
342+
message: "KeyValueFileReader does not support deletion vectors".to_string(),
343+
})?;
344+
}
333345
}
334-
335346
// Create one stream per data file.
336347
let mut file_streams: Vec<ArrowRecordBatchStream> = Vec::new();
337348

338-
for file_meta in split.data_files().to_vec() {
349+
for split in split_group {
350+
for file_meta in split.data_files().to_vec() {
339351
let data_fields: Option<Vec<DataField>> = if file_meta.schema_id != table_schema_id {
340352
let data_schema = schema_manager.schema(file_meta.schema_id).await?;
341353
Some(data_schema.fields().to_vec())
@@ -357,9 +369,10 @@ impl KeyValueFileReader {
357369
file_meta,
358370
data_fields,
359371
None,
360-
None,
372+
split.row_ranges().map(|ranges| ranges.to_vec()),
361373
)?;
362374
file_streams.push(stream);
375+
}
363376
}
364377

365378
if file_streams.is_empty() {

0 commit comments

Comments
 (0)