@@ -14,9 +14,10 @@ use std::collections::HashMap;
1414use std:: sync:: Arc ;
1515use std:: time:: { SystemTime , UNIX_EPOCH } ;
1616
17+ use arrow:: compute:: cast;
1718use arrow_array:: RecordBatch ;
1819use arrow_ipc:: writer:: StreamWriter ;
19- use arrow_schema:: Schema as ArrowSchema ;
20+ use arrow_schema:: { DataType , Field , FieldRef , Schema as ArrowSchema , TimeUnit } ;
2021use deltalake:: errors:: DeltaTableError ;
2122use deltalake:: kernel:: engine:: arrow_conversion:: TryIntoKernel as _;
2223use deltalake:: kernel:: schema:: cast:: normalize_for_delta;
@@ -27,8 +28,42 @@ use deltalake::{DeltaTable, open_table_with_storage_options};
2728use tracing:: { info, instrument} ;
2829use url:: Url ;
2930
31+ use crate :: sql:: common:: { TIMESTAMP_FIELD , UPDATING_META_FIELD } ;
32+
3033use super :: DeltaSinkError ;
3134
35+ /// Streaming-internal columns that must not be persisted to external sinks.
36+ pub fn is_streaming_system_column ( name : & str ) -> bool {
37+ name == TIMESTAMP_FIELD || name == UPDATING_META_FIELD
38+ }
39+
40+ /// Remove `_timestamp` / `_updating_meta` from a schema (e.g. connector `fs_schema` may inject them).
41+ pub fn strip_streaming_system_columns ( schema : & ArrowSchema ) -> ArrowSchema {
42+ let fields: Vec < FieldRef > = schema
43+ . fields ( )
44+ . iter ( )
45+ . filter ( |f| !is_streaming_system_column ( f. name ( ) ) )
46+ . cloned ( )
47+ . collect ( ) ;
48+ ArrowSchema :: new ( fields)
49+ }
50+
51+ pub fn strip_streaming_system_columns_arc ( schema : Arc < ArrowSchema > ) -> Option < Arc < ArrowSchema > > {
52+ let stripped = strip_streaming_system_columns ( schema. as_ref ( ) ) ;
53+ if stripped. fields ( ) . is_empty ( ) {
54+ return None ;
55+ }
56+ let had_system = schema
57+ . fields ( )
58+ . iter ( )
59+ . any ( |f| is_streaming_system_column ( f. name ( ) ) ) ;
60+ if had_system {
61+ Some ( Arc :: new ( stripped) )
62+ } else {
63+ Some ( schema)
64+ }
65+ }
66+
3267pub struct UncommittedDataFile {
3368 pub path : String ,
3469 pub size_bytes : u64 ,
@@ -42,6 +77,8 @@ pub struct DeltaTableCommitter {
4277 table : Option < DeltaTable > ,
4378 /// Precomputed from catalog `fs_schema` at startup, or lazily from the first batch.
4479 delta_columns : Option < Vec < StructField > > ,
80+ /// Arrow 55 schema for Parquet writes (timestamps normalized to microsecond).
81+ write_schema : Option < Arc < ArrowSchema > > ,
4582}
4683
4784impl DeltaTableCommitter {
@@ -50,7 +87,11 @@ impl DeltaTableCommitter {
5087 storage_options : HashMap < String , String > ,
5188 catalog_schema : Option < Arc < ArrowSchema > > ,
5289 ) -> Result < Self , DeltaSinkError > {
53- let delta_columns = catalog_schema
90+ let user_schema = catalog_schema. and_then ( strip_streaming_system_columns_arc) ;
91+ let write_schema = user_schema
92+ . as_ref ( )
93+ . map ( |s| Arc :: new ( normalize_arrow_schema_for_delta ( s) ) ) ;
94+ let delta_columns = user_schema
5495 . as_deref ( )
5596 . map ( arrow_schema_to_delta_columns)
5697 . transpose ( ) ?;
@@ -61,13 +102,30 @@ impl DeltaTableCommitter {
61102 uncommitted : Vec :: new ( ) ,
62103 table : None ,
63104 delta_columns,
105+ write_schema,
64106 } )
65107 }
66108
67- /// Fallback when catalog schema is absent: derive columns from the first flushed batch.
109+ pub fn write_schema ( & self ) -> Option < Arc < ArrowSchema > > {
110+ self . write_schema . clone ( )
111+ }
112+
113+ /// Fallback when catalog schema is absent: derive user columns from the first flushed batch.
68114 pub fn update_schema ( & mut self , schema : Arc < ArrowSchema > ) -> Result < ( ) , DeltaSinkError > {
115+ let user_schema = strip_streaming_system_columns_arc ( schema) . ok_or_else ( || {
116+ DeltaSinkError :: CommitterFailed (
117+ "cannot derive delta table schema: no user columns after removing streaming \
118+ system columns (_timestamp, _updating_meta)"
119+ . into ( ) ,
120+ )
121+ } ) ?;
69122 if self . delta_columns . is_none ( ) {
70- self . delta_columns = Some ( arrow_schema_to_delta_columns ( & schema) ?) ;
123+ self . delta_columns = Some ( arrow_schema_to_delta_columns ( user_schema. as_ref ( ) ) ?) ;
124+ }
125+ if self . write_schema . is_none ( ) {
126+ self . write_schema = Some ( Arc :: new ( normalize_arrow_schema_for_delta (
127+ user_schema. as_ref ( ) ,
128+ ) ) ) ;
71129 }
72130 Ok ( ( ) )
73131 }
@@ -231,6 +289,105 @@ impl DeltaTableCommitter {
231289 }
232290}
233291
292+ /// Normalize Arrow 55 schema for Delta Parquet writes (align with deltalake `normalize_for_delta`).
293+ pub fn normalize_arrow_schema_for_delta ( schema : & ArrowSchema ) -> ArrowSchema {
294+ let fields: Vec < FieldRef > = schema
295+ . fields ( )
296+ . iter ( )
297+ . map ( |f| Arc :: new ( normalize_field_for_delta ( f. as_ref ( ) ) ) )
298+ . collect ( ) ;
299+ ArrowSchema :: new ( fields)
300+ }
301+
302+ fn normalize_field_for_delta ( field : & Field ) -> Field {
303+ let data_type = normalize_datatype_for_delta ( field. data_type ( ) ) ;
304+ if data_type == * field. data_type ( ) {
305+ field. clone ( )
306+ } else {
307+ field. clone ( ) . with_data_type ( data_type)
308+ }
309+ }
310+
311+ fn normalize_datatype_for_delta ( dt : & DataType ) -> DataType {
312+ match dt {
313+ DataType :: Date64 => DataType :: Date32 ,
314+ DataType :: Timestamp ( TimeUnit :: Second , tz)
315+ | DataType :: Timestamp ( TimeUnit :: Millisecond , tz)
316+ | DataType :: Timestamp ( TimeUnit :: Nanosecond , tz) => {
317+ DataType :: Timestamp ( TimeUnit :: Microsecond , tz. clone ( ) )
318+ }
319+ DataType :: Struct ( fields) => {
320+ let normalized: Vec < FieldRef > = fields
321+ . iter ( )
322+ . map ( |f| Arc :: new ( normalize_field_for_delta ( f. as_ref ( ) ) ) )
323+ . collect ( ) ;
324+ DataType :: Struct ( normalized. into ( ) )
325+ }
326+ DataType :: List ( inner) => {
327+ DataType :: List ( Arc :: new ( normalize_field_for_delta ( inner. as_ref ( ) ) ) )
328+ }
329+ DataType :: LargeList ( inner) => {
330+ DataType :: LargeList ( Arc :: new ( normalize_field_for_delta ( inner. as_ref ( ) ) ) )
331+ }
332+ DataType :: FixedSizeList ( inner, len) => {
333+ DataType :: FixedSizeList ( Arc :: new ( normalize_field_for_delta ( inner. as_ref ( ) ) ) , * len)
334+ }
335+ DataType :: Map ( entries, sorted) => DataType :: Map (
336+ Arc :: new ( normalize_field_for_delta ( entries. as_ref ( ) ) ) ,
337+ * sorted,
338+ ) ,
339+ _ => dt. clone ( ) ,
340+ }
341+ }
342+
343+ /// Cast record batches so on-disk Parquet matches the Delta table schema.
344+ pub fn cast_batches_for_delta_write (
345+ batches : & [ RecordBatch ] ,
346+ target_schema : & ArrowSchema ,
347+ ) -> Result < Vec < RecordBatch > , DeltaSinkError > {
348+ let target = Arc :: new ( target_schema. clone ( ) ) ;
349+ batches
350+ . iter ( )
351+ . map ( |batch| cast_batch_for_delta_write ( batch, & target) )
352+ . collect ( )
353+ }
354+
355+ fn cast_batch_for_delta_write (
356+ batch : & RecordBatch ,
357+ target_schema : & Arc < ArrowSchema > ,
358+ ) -> Result < RecordBatch , DeltaSinkError > {
359+ if batch. schema ( ) . as_ref ( ) == target_schema. as_ref ( ) {
360+ return Ok ( batch. clone ( ) ) ;
361+ }
362+
363+ let mut columns = Vec :: with_capacity ( target_schema. fields ( ) . len ( ) ) ;
364+ for field in target_schema. fields ( ) {
365+ let col = batch. column_by_name ( field. name ( ) ) . ok_or_else ( || {
366+ DeltaSinkError :: CommitterFailed ( format ! (
367+ "batch missing column '{}' required by delta write schema" ,
368+ field. name( )
369+ ) )
370+ } ) ?;
371+ let casted = if col. data_type ( ) == field. data_type ( ) {
372+ col. clone ( )
373+ } else {
374+ cast ( col, field. data_type ( ) ) . map_err ( |e| {
375+ DeltaSinkError :: CommitterFailed ( format ! (
376+ "failed to cast column '{}' from {:?} to {:?}: {e}" ,
377+ field. name( ) ,
378+ col. data_type( ) ,
379+ field. data_type( )
380+ ) )
381+ } ) ?
382+ } ;
383+ columns. push ( casted) ;
384+ }
385+
386+ RecordBatch :: try_new ( target_schema. clone ( ) , columns) . map_err ( |e| {
387+ DeltaSinkError :: CommitterFailed ( format ! ( "failed to build delta write batch: {e}" ) )
388+ } )
389+ }
390+
234391/// Bridge arrow 55 (runtime) schema to deltalake kernel schema via IPC.
235392fn arrow_schema_to_delta_columns ( schema : & ArrowSchema ) -> Result < Vec < StructField > , DeltaSinkError > {
236393 let empty = RecordBatch :: new_empty ( Arc :: new ( schema. clone ( ) ) ) ;
0 commit comments