Skip to content

Commit 677cc06

Browse files
authored
feat: support existing changelog incremental reads (#509)
1 parent f55ad02 commit 677cc06

9 files changed

Lines changed: 886 additions & 16 deletions

File tree

crates/paimon/src/spec/schema.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,11 @@ pub const VALUE_KIND_FIELD_NAME: &str = "_VALUE_KIND";
811811
/// Must match Java Paimon's `SpecialFields.VALUE_KIND` (Integer.MAX_VALUE - 2).
812812
pub const VALUE_KIND_FIELD_ID: i32 = i32::MAX - 2;
813813

814+
pub const ROW_KIND_FIELD_NAME: &str = "rowkind";
815+
816+
/// Must match Java Paimon's `SpecialFields.ROW_KIND` (Integer.MAX_VALUE - 4).
817+
pub const ROW_KIND_FIELD_ID: i32 = i32::MAX - 4;
818+
814819
/// Data field for paimon table.
815820
///
816821
/// Impl Reference: <https://github.com/apache/paimon/blob/release-0.8.2/paimon-common/src/main/java/org/apache/paimon/types/DataField.java#L40>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use super::incremental_scan::{IncrementalPlan, IncrementalScan, IncrementalScanMode};
19+
use super::{ArrowRecordBatchStream, Table};
20+
use crate::spec::{
21+
BigIntType, DataField, DataType, VarCharType, ROW_KIND_FIELD_ID, ROW_KIND_FIELD_NAME,
22+
SEQUENCE_NUMBER_FIELD_ID, SEQUENCE_NUMBER_FIELD_NAME,
23+
};
24+
25+
/// Wrapper that exposes table rows with a leading `rowkind` audit column.
26+
///
27+
/// Incremental reads produce:
28+
/// - Delta: primary-key rows use physical `_VALUE_KIND`; append rows are `+I`
29+
/// - Changelog: kinds come from physical `_VALUE_KIND` (`+I`/`-U`/`+U`/`-D`)
30+
/// - Diff: not implemented in this release
31+
#[derive(Debug, Clone)]
32+
pub struct AuditLogTable {
33+
wrapped: Table,
34+
}
35+
36+
const TABLE_READ_SEQUENCE_NUMBER_ENABLED: &str = "table-read.sequence-number.enabled";
37+
38+
impl AuditLogTable {
39+
pub fn new(wrapped: Table) -> Self {
40+
Self { wrapped }
41+
}
42+
43+
pub fn wrapped(&self) -> &Table {
44+
&self.wrapped
45+
}
46+
47+
/// Logical fields: `rowkind` (+ optional `_SEQUENCE_NUMBER`) then table fields.
48+
pub fn fields(&self) -> crate::Result<Vec<DataField>> {
49+
let mut fields = Vec::with_capacity(self.wrapped.schema().fields().len() + 2);
50+
fields.push(DataField::new(
51+
ROW_KIND_FIELD_ID,
52+
ROW_KIND_FIELD_NAME.to_string(),
53+
DataType::VarChar(VarCharType::string_type()),
54+
));
55+
if self.sequence_number_enabled() {
56+
fields.push(DataField::new(
57+
SEQUENCE_NUMBER_FIELD_ID,
58+
SEQUENCE_NUMBER_FIELD_NAME.to_string(),
59+
DataType::BigInt(BigIntType::new()),
60+
));
61+
}
62+
fields.extend(self.wrapped.schema().fields().iter().cloned());
63+
Ok(fields)
64+
}
65+
66+
fn sequence_number_enabled(&self) -> bool {
67+
self.wrapped
68+
.schema()
69+
.options()
70+
.get(TABLE_READ_SEQUENCE_NUMBER_ENABLED)
71+
.is_some_and(|v| v.eq_ignore_ascii_case("true"))
72+
}
73+
74+
pub fn new_incremental_scan(
75+
&self,
76+
mode: IncrementalScanMode,
77+
start_exclusive: i64,
78+
end_inclusive: i64,
79+
) -> IncrementalScan<'_> {
80+
IncrementalScan::for_table(&self.wrapped, mode, start_exclusive, end_inclusive)
81+
}
82+
83+
pub fn to_arrow(&self, plan: &IncrementalPlan) -> crate::Result<ArrowRecordBatchStream> {
84+
let read = self.wrapped.new_read_builder().new_read()?;
85+
read.to_audit_log_arrow(plan)
86+
}
87+
}

crates/paimon/src/table/incremental_scan.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ use crate::spec::{CommitKind, CoreOptions};
2626
pub enum IncrementalScanMode {
2727
/// Read data files from APPEND snapshots in the range (delta manifests).
2828
Delta,
29-
/// Read changelog manifest files in the range.
29+
/// Read existing changelog manifest files in the range.
3030
///
31-
/// Not fully implemented in this release; planning returns
32-
/// [`Error::Unsupported`](crate::Error::Unsupported).
31+
/// Skips [`OVERWRITE`](crate::spec::CommitKind::OVERWRITE) snapshots and
32+
/// snapshots without a `changelog_manifest_list`. Does not generate
33+
/// changelogs (no compact/lookup producer path).
3334
Changelog,
3435
/// Resolve to [`Delta`](Self::Delta) when `changelog-producer=none`,
3536
/// otherwise to [`Changelog`](Self::Changelog).
@@ -204,10 +205,21 @@ impl<'a> IncrementalScan<'a> {
204205
}
205206

206207
async fn plan_changelog(&self, mode: IncrementalScanMode) -> crate::Result<IncrementalPlan> {
207-
let _ = mode;
208-
Err(crate::Error::Unsupported {
209-
message: "Batch incremental Changelog scan is not implemented yet".to_string(),
210-
})
208+
let mut splits = Vec::new();
209+
for snapshot_id in (self.start_exclusive + 1)..=self.end_inclusive {
210+
let snapshot = self.snapshot_manager.get_snapshot(snapshot_id).await?;
211+
// OVERWRITE rewrites table contents and does not contribute changelog
212+
// files for batch incremental reads (Java IncrementalChangelogStartingScanner).
213+
if snapshot.commit_kind() == &CommitKind::OVERWRITE {
214+
continue;
215+
}
216+
if snapshot.changelog_manifest_list().is_none() {
217+
continue;
218+
}
219+
let plan = self.scan.plan_snapshot_changelog(&snapshot).await?;
220+
splits.extend(plan.splits().iter().cloned().map(IncrementalSplit::Data));
221+
}
222+
Ok(IncrementalPlan::new(mode, splits))
211223
}
212224

213225
async fn plan_diff(&self, mode: IncrementalScanMode) -> crate::Result<IncrementalPlan> {

crates/paimon/src/table/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
//! Table API for Apache Paimon
1919
2020
pub(crate) mod aggregator;
21+
mod audit_log_table;
2122
pub(crate) mod bin_pack;
2223
mod bitmap_global_index_reader;
2324
mod blob_resolver;
@@ -85,6 +86,7 @@ mod write_builder;
8586

8687
use crate::Result;
8788
use arrow_array::RecordBatch;
89+
pub use audit_log_table::AuditLogTable;
8890
pub use branch_manager::BranchManager;
8991
pub use btree_global_index_build_builder::BTreeGlobalIndexBuildBuilder;
9092
pub use commit_message::CommitMessage;

0 commit comments

Comments
 (0)