@@ -22,8 +22,17 @@ use super::incremental_scan::{IncrementalPlan, IncrementalScanMode, IncrementalS
2222use super :: kv_file_reader:: { KeyValueFileReader , KeyValueReadConfig } ;
2323use super :: read_builder:: split_scan_predicates;
2424use 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+ } ;
2631use 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
0 commit comments