Skip to content

Commit 10fe992

Browse files
committed
feat: support batch incremental diff reads
1 parent 0d927e1 commit 10fe992

7 files changed

Lines changed: 1323 additions & 34 deletions

File tree

crates/paimon/src/spec/core_options.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ const IGNORE_DELETE_FALLBACK_KEYS: &[&str] = &[
8686
"deduplicate.ignore-delete",
8787
"partial-update.ignore-delete",
8888
];
89+
const DIFF_PARALLELISM_OPTION: &str = "diff.parallelism";
90+
const DEFAULT_DIFF_PARALLELISM: usize = 4;
8991
const DEFAULT_COMMIT_MAX_RETRIES: u32 = 10;
9092
const DEFAULT_COMMIT_TIMEOUT_MS: u64 = 120_000;
9193
const DEFAULT_COMMIT_MIN_RETRY_WAIT_MS: u64 = 1_000;
@@ -469,6 +471,17 @@ impl<'a> CoreOptions<'a> {
469471
.is_some_and(|v| v.eq_ignore_ascii_case("true"))
470472
}
471473

474+
/// Parallelism for batch incremental Diff pair reads (`diff.parallelism`).
475+
///
476+
/// Default is 4; values below 1 are clamped to 1.
477+
pub fn diff_parallelism(&self) -> usize {
478+
self.options
479+
.get(DIFF_PARALLELISM_OPTION)
480+
.and_then(|s| s.parse().ok())
481+
.unwrap_or(DEFAULT_DIFF_PARALLELISM)
482+
.max(1)
483+
}
484+
472485
pub fn data_evolution_enabled(&self) -> bool {
473486
self.options
474487
.get(DATA_EVOLUTION_ENABLED_OPTION)
@@ -1481,6 +1494,21 @@ mod tests {
14811494
);
14821495
}
14831496

1497+
#[test]
1498+
fn test_diff_parallelism_defaults() {
1499+
let options = HashMap::new();
1500+
let core = CoreOptions::new(&options);
1501+
assert_eq!(core.diff_parallelism(), 4);
1502+
1503+
let options = HashMap::from([(DIFF_PARALLELISM_OPTION.to_string(), "0".into())]);
1504+
let core = CoreOptions::new(&options);
1505+
assert_eq!(core.diff_parallelism(), 1);
1506+
1507+
let options = HashMap::from([(DIFF_PARALLELISM_OPTION.to_string(), "8".into())]);
1508+
let core = CoreOptions::new(&options);
1509+
assert_eq!(core.diff_parallelism(), 8);
1510+
}
1511+
14841512
#[test]
14851513
fn test_changelog_producer_accepts_known_values() {
14861514
for (value, expected) in [

crates/paimon/src/table/audit_log_table.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::spec::{
2727
/// Incremental reads produce:
2828
/// - Delta: primary-key rows use physical `_VALUE_KIND`; append rows are `+I`
2929
/// - Changelog: kinds come from physical `_VALUE_KIND` (`+I`/`-U`/`+U`/`-D`)
30-
/// - Diff: not implemented in this release
30+
/// - Diff: before/after image comparison (`+I`/`-U`/`+U`/`-D`, equal keys skipped)
3131
#[derive(Debug, Clone)]
3232
pub struct AuditLogTable {
3333
wrapped: Table,

crates/paimon/src/table/incremental_scan.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ pub enum IncrementalScanMode {
3535
/// Resolve to [`Delta`](Self::Delta) when `changelog-producer=none`,
3636
/// otherwise to [`Changelog`](Self::Changelog).
3737
Auto,
38-
/// Diff before/after snapshots.
38+
/// Diff before/after snapshot states for PK tables.
3939
///
40-
/// Not fully implemented in this release; planning returns
41-
/// [`Error::Unsupported`](crate::Error::Unsupported).
40+
/// Phase 1 supports only `merge-engine=deduplicate`. Planning compares the
41+
/// full table state at `start_exclusive` vs `end_inclusive` and yields
42+
/// per-(partition, bucket) [`IncrementalSplit::DiffPair`] units.
4243
Diff,
4344
}
4445

@@ -223,9 +224,51 @@ impl<'a> IncrementalScan<'a> {
223224
}
224225

225226
async fn plan_diff(&self, mode: IncrementalScanMode) -> crate::Result<IncrementalPlan> {
226-
let _ = mode;
227-
Err(crate::Error::Unsupported {
228-
message: "Batch incremental Diff scan is not implemented yet".to_string(),
229-
})
227+
let core_options = CoreOptions::new(self.table.schema().options());
228+
if core_options.merge_engine()? != crate::spec::MergeEngine::Deduplicate {
229+
return Err(crate::Error::Unsupported {
230+
message: "Batch incremental Diff only supports merge-engine=deduplicate in Phase 1"
231+
.to_string(),
232+
});
233+
}
234+
let before = self
235+
.snapshot_manager
236+
.get_snapshot(self.start_exclusive)
237+
.await?;
238+
let after = self
239+
.snapshot_manager
240+
.get_snapshot(self.end_inclusive)
241+
.await?;
242+
let (before_plan, after_plan) = self.scan.plan_snapshot_diff(&before, &after).await?;
243+
244+
use std::collections::BTreeMap;
245+
type PBKey = (Vec<u8>, i32);
246+
247+
let mut before_map: BTreeMap<PBKey, Vec<DataSplit>> = BTreeMap::new();
248+
for split in before_plan.splits() {
249+
let key = (split.partition().to_serialized_bytes(), split.bucket());
250+
before_map.entry(key).or_default().push(split.clone());
251+
}
252+
253+
let mut after_map: BTreeMap<PBKey, Vec<DataSplit>> = BTreeMap::new();
254+
for split in after_plan.splits() {
255+
let key = (split.partition().to_serialized_bytes(), split.bucket());
256+
after_map.entry(key).or_default().push(split.clone());
257+
}
258+
259+
let mut keys: std::collections::BTreeSet<PBKey> = before_map.keys().cloned().collect();
260+
keys.extend(after_map.keys().cloned());
261+
262+
let mut splits = Vec::new();
263+
for key in keys {
264+
let before = before_map.remove(&key).unwrap_or_default();
265+
let after = after_map.remove(&key).unwrap_or_default();
266+
if before.is_empty() && after.is_empty() {
267+
continue;
268+
}
269+
splits.push(IncrementalSplit::DiffPair { before, after });
270+
}
271+
272+
Ok(IncrementalPlan::new(mode, splits))
230273
}
231274
}

0 commit comments

Comments
 (0)