Skip to content

Commit 65c2480

Browse files
committed
feat: support existing changelog incremental reads
1 parent 20cdfe9 commit 65c2480

8 files changed

Lines changed: 771 additions & 16 deletions

File tree

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, SEQUENCE_NUMBER_FIELD_ID,
22+
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: every row is `+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+
-1,
52+
"rowkind".to_string(),
53+
DataType::VarChar(VarCharType::new(8)?),
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;
@@ -82,6 +83,7 @@ mod write_builder;
8283

8384
use crate::Result;
8485
use arrow_array::RecordBatch;
86+
pub use audit_log_table::AuditLogTable;
8587
pub use branch_manager::BranchManager;
8688
pub use btree_global_index_build_builder::BTreeGlobalIndexBuildBuilder;
8789
pub use commit_message::CommitMessage;

crates/paimon/src/table/table_read.rs

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,17 @@ use super::incremental_scan::{IncrementalPlan, IncrementalScanMode, IncrementalS
2222
use super::kv_file_reader::{KeyValueFileReader, KeyValueReadConfig};
2323
use super::read_builder::split_scan_predicates;
2424
use super::{ArrowRecordBatchStream, Table};
25-
use crate::spec::{CoreOptions, DataField, MergeEngine, Predicate};
25+
use crate::arrow::build_target_arrow_schema;
26+
use crate::spec::{
27+
BigIntType, CoreOptions, DataField, DataType, MergeEngine, Predicate, TinyIntType,
28+
SEQUENCE_NUMBER_FIELD_ID, SEQUENCE_NUMBER_FIELD_NAME, VALUE_KIND_FIELD_ID,
29+
VALUE_KIND_FIELD_NAME,
30+
};
2631
use crate::DataSplit;
32+
use arrow_array::{ArrayRef, RecordBatch, StringArray};
33+
use arrow_schema::Schema as ArrowSchema;
34+
use futures::StreamExt;
35+
use std::sync::Arc;
2736

2837
/// Table read: reads data from splits (e.g. produced by [TableScan::plan]).
2938
///
@@ -124,6 +133,23 @@ impl<'a> TableRead<'a> {
124133
}),
125134
}
126135
}
136+
137+
/// Returns an audit-log [`ArrowRecordBatchStream`] for an incremental plan.
138+
///
139+
/// Output schema is `rowkind` (+ optional `_SEQUENCE_NUMBER`) followed by
140+
/// the projected user columns. Delta rows are all `+I`; Changelog rows take
141+
/// kinds from `_VALUE_KIND`. Diff remains unsupported.
142+
pub fn to_audit_log_arrow(
143+
&self,
144+
plan: &IncrementalPlan,
145+
) -> crate::Result<ArrowRecordBatchStream> {
146+
match &self.0 {
147+
TableReadKind::Paimon(read) => read.to_audit_log_arrow(plan),
148+
TableReadKind::Format(_) => Err(crate::Error::Unsupported {
149+
message: "Format tables do not support audit log batch read".to_string(),
150+
}),
151+
}
152+
}
127153
}
128154

129155
#[derive(Debug, Clone)]
@@ -205,6 +231,109 @@ impl<'a> PaimonTableRead<'a> {
205231
self.new_data_file_reader().read(&data_splits)
206232
}
207233

234+
/// Returns an audit-log stream for a planned incremental scan.
235+
pub fn to_audit_log_arrow(
236+
&self,
237+
plan: &IncrementalPlan,
238+
) -> crate::Result<ArrowRecordBatchStream> {
239+
match plan.mode() {
240+
IncrementalScanMode::Diff => Err(crate::Error::Unsupported {
241+
message: "Batch incremental Diff audit read not yet implemented".to_string(),
242+
}),
243+
IncrementalScanMode::Delta => self.audit_raw_stream(plan, false),
244+
IncrementalScanMode::Changelog => self.audit_raw_stream(plan, true),
245+
IncrementalScanMode::Auto => unreachable!("Auto resolved during plan()"),
246+
}
247+
}
248+
249+
fn audit_raw_stream(
250+
&self,
251+
plan: &IncrementalPlan,
252+
has_value_kind: bool,
253+
) -> crate::Result<ArrowRecordBatchStream> {
254+
let data_splits = plan.data_splits();
255+
let user_read_type = self.read_type.clone();
256+
let include_sequence = audit_sequence_number_enabled(self.table);
257+
let audit_schema = audit_schema_for_read_type(&user_read_type, include_sequence)?;
258+
259+
let mut read_type = user_read_type.clone();
260+
if include_sequence {
261+
read_type.insert(
262+
0,
263+
DataField::new(
264+
SEQUENCE_NUMBER_FIELD_ID,
265+
SEQUENCE_NUMBER_FIELD_NAME.to_string(),
266+
DataType::BigInt(BigIntType::new()),
267+
),
268+
);
269+
}
270+
if has_value_kind {
271+
read_type.push(DataField::new(
272+
VALUE_KIND_FIELD_ID,
273+
VALUE_KIND_FIELD_NAME.to_string(),
274+
DataType::TinyInt(TinyIntType::new()),
275+
));
276+
}
277+
278+
let reader = DataFileReader::new(
279+
self.table.file_io.clone(),
280+
self.table.schema_manager().clone(),
281+
self.table.schema().id(),
282+
self.table.schema.fields().to_vec(),
283+
read_type,
284+
self.data_predicates.clone(),
285+
);
286+
let raw_stream = reader.read(&data_splits)?;
287+
288+
Ok(Box::pin(async_stream::try_stream! {
289+
futures::pin_mut!(raw_stream);
290+
while let Some(batch) = raw_stream.next().await {
291+
let batch = batch?;
292+
let rowkind_col: ArrayRef = if has_value_kind {
293+
let col = batch
294+
.column_by_name(VALUE_KIND_FIELD_NAME)
295+
.ok_or_else(|| crate::Error::DataInvalid {
296+
message: "Changelog audit read missing _VALUE_KIND column".to_string(),
297+
source: None,
298+
})?;
299+
Arc::new(rowkind_array_from_column(col)?)
300+
} else {
301+
let inserts: Vec<&'static str> = (0..batch.num_rows()).map(|_| "+I").collect();
302+
Arc::new(StringArray::from(inserts))
303+
};
304+
305+
let mut columns: Vec<ArrayRef> = vec![rowkind_col];
306+
if include_sequence {
307+
let seq_col = batch
308+
.column_by_name(SEQUENCE_NUMBER_FIELD_NAME)
309+
.ok_or_else(|| crate::Error::DataInvalid {
310+
message: "Audit read missing _SEQUENCE_NUMBER column".to_string(),
311+
source: None,
312+
})?;
313+
columns.push(seq_col.clone());
314+
}
315+
for field in &user_read_type {
316+
let col = batch
317+
.column_by_name(field.name())
318+
.ok_or_else(|| crate::Error::DataInvalid {
319+
message: format!(
320+
"Audit read missing column '{}'",
321+
field.name()
322+
),
323+
source: None,
324+
})?;
325+
columns.push(col.clone());
326+
}
327+
yield RecordBatch::try_new(audit_schema.clone(), columns).map_err(|e| {
328+
crate::Error::UnexpectedError {
329+
message: format!("Failed to build audit log batch: {e}"),
330+
source: Some(Box::new(e)),
331+
}
332+
})?;
333+
}
334+
}))
335+
}
336+
208337
/// Returns an [`ArrowRecordBatchStream`].
209338
pub fn to_arrow(&self, data_splits: &[DataSplit]) -> crate::Result<ArrowRecordBatchStream> {
210339
let has_primary_keys = !self.table.schema.primary_keys().is_empty();
@@ -352,6 +481,55 @@ impl<'a> PaimonTableRead<'a> {
352481
}
353482
}
354483

484+
fn audit_schema_for_read_type(
485+
read_type: &[DataField],
486+
include_sequence: bool,
487+
) -> crate::Result<Arc<ArrowSchema>> {
488+
let mut fields = Vec::with_capacity(read_type.len() + 2);
489+
fields.push(DataField::new(
490+
-1,
491+
"rowkind".to_string(),
492+
DataType::VarChar(crate::spec::VarCharType::new(8)?),
493+
));
494+
if include_sequence {
495+
fields.push(DataField::new(
496+
SEQUENCE_NUMBER_FIELD_ID,
497+
SEQUENCE_NUMBER_FIELD_NAME.to_string(),
498+
DataType::BigInt(BigIntType::new()),
499+
));
500+
}
501+
fields.extend(read_type.iter().cloned());
502+
build_target_arrow_schema(&fields)
503+
}
504+
505+
fn audit_sequence_number_enabled(table: &Table) -> bool {
506+
table
507+
.schema()
508+
.options()
509+
.get("table-read.sequence-number.enabled")
510+
.is_some_and(|v| v.eq_ignore_ascii_case("true"))
511+
}
512+
513+
fn rowkind_array_from_column(column: &dyn arrow_array::Array) -> crate::Result<StringArray> {
514+
let values = column
515+
.as_any()
516+
.downcast_ref::<arrow_array::Int8Array>()
517+
.ok_or_else(|| crate::Error::DataInvalid {
518+
message: "AuditLogTable _VALUE_KIND column must be Int8".to_string(),
519+
source: None,
520+
})?;
521+
let strings: Vec<&'static str> = (0..values.len())
522+
.map(|idx| match values.value(idx) {
523+
0 => "+I",
524+
1 => "-U",
525+
2 => "+U",
526+
3 => "-D",
527+
_ => "?",
528+
})
529+
.collect();
530+
Ok(StringArray::from(strings))
531+
}
532+
355533
/// Whether a primary-key split must go through the sort-merge reader.
356534
///
357535
/// Mirrors Java `PrimaryKeyTableRawFileSplitReadProvider#match`: a raw read

crates/paimon/src/table/table_scan.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,16 @@ impl<'a> TableScan<'a> {
716716
}
717717
}
718718

719+
/// Plan data splits from a snapshot's changelog manifest list only.
720+
pub(crate) async fn plan_snapshot_changelog(&self, snapshot: &Snapshot) -> crate::Result<Plan> {
721+
match &self.0 {
722+
TableScanKind::Paimon(scan) => scan.plan_snapshot_changelog(snapshot).await,
723+
TableScanKind::Format(_) => Err(crate::Error::Unsupported {
724+
message: "Format tables do not support incremental changelog scan".to_string(),
725+
}),
726+
}
727+
}
728+
719729
#[cfg(test)]
720730
fn apply_limit_pushdown(&self, splits: Vec<DataSplit>) -> Vec<DataSplit> {
721731
match &self.0 {
@@ -1077,6 +1087,27 @@ impl<'a> PaimonTableScan<'a> {
10771087
.await
10781088
}
10791089

1090+
/// Plan data splits from a snapshot's changelog manifest list.
1091+
///
1092+
/// Reuses the same split-building path as a full snapshot plan, but only
1093+
/// reads the changelog manifest list and keeps ADD entries. Snapshots
1094+
/// without a changelog list yield an empty plan.
1095+
pub(crate) async fn plan_snapshot_changelog(&self, snapshot: &Snapshot) -> crate::Result<Plan> {
1096+
self.ensure_query_auth_allowed()?;
1097+
let Some(list_name) = snapshot.changelog_manifest_list() else {
1098+
return Ok(Plan::new(Vec::new()));
1099+
};
1100+
let entries = self.plan_manifest_list_entries(list_name).await?;
1101+
let data_evolution_read_field_ids = self.projected_read_field_ids()?;
1102+
self.plan_snapshot_from_entries(
1103+
snapshot.clone(),
1104+
entries,
1105+
data_evolution_read_field_ids.as_ref(),
1106+
None,
1107+
)
1108+
.await
1109+
}
1110+
10801111
/// Read entries from a single manifest list (delta or changelog) with
10811112
/// partition / bucket filter pushdown matching the full scan path.
10821113
async fn plan_manifest_list_entries(

0 commit comments

Comments
 (0)