@@ -26,7 +26,9 @@ use std::collections::{HashMap, HashSet};
2626use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
2727use std:: sync:: Arc ;
2828
29- use datafusion:: arrow:: array:: { Array , Int32Array , RecordBatch , UInt32Array , UInt64Array } ;
29+ use datafusion:: arrow:: array:: {
30+ Array , ArrayRef , Int32Array , RecordBatch , UInt32Array , UInt64Array ,
31+ } ;
3032use datafusion:: arrow:: compute;
3133use datafusion:: arrow:: datatypes:: { DataType as ArrowDataType , Field , Schema } ;
3234use datafusion:: error:: { DataFusionError , Result as DFResult } ;
@@ -37,7 +39,7 @@ use datafusion::sql::sqlparser::ast::{
3739} ;
3840use futures:: TryStreamExt ;
3941
40- use paimon:: spec:: { datums_to_binary_row, extract_datum_from_arrow, CoreOptions } ;
42+ use paimon:: spec:: { datums_to_binary_row, extract_datum_from_arrow, CoreOptions , DataField } ;
4143use paimon:: table:: { CopyOnWriteMergeWriter , DataSplitBuilder , Table , WriteBuilder } ;
4244
4345use crate :: error:: to_datafusion_error;
@@ -569,13 +571,6 @@ async fn execute_cow_merge_inner(
569571
570572 // Handle NOT MATCHED → INSERT
571573 if !clauses. inserts . is_empty ( ) {
572- let table_fields: Vec < String > = table
573- . schema ( )
574- . fields ( )
575- . iter ( )
576- . map ( |f| f. name ( ) . to_string ( ) )
577- . collect ( ) ;
578-
579574 let insert_sql = if has_target_data {
580575 format ! (
581576 "SELECT {s_alias}.* FROM {source_ref} AS {s_alias} \
@@ -595,7 +590,7 @@ async fn execute_cow_merge_inner(
595590 & clauses. inserts ,
596591 s_alias,
597592 & [ ] ,
598- & table_fields ,
593+ table . schema ( ) . fields ( ) ,
599594 temp_tracker,
600595 )
601596 . await ?;
@@ -734,21 +729,14 @@ async fn execute_merge_into_once(
734729 injected_columns. push ( format ! ( "__upd_{col}" ) ) ;
735730 }
736731 }
737- // Table schema field names for reordering INSERT columns
738- let table_fields: Vec < String > = table
739- . schema ( )
740- . fields ( )
741- . iter ( )
742- . map ( |f| f. name ( ) . to_string ( ) )
743- . collect ( ) ;
744732 let mut temp_tracker = TempTableTracker :: new ( ctx) ;
745733 let insert_batches = build_insert_batches (
746734 ctx,
747735 & not_matched_batches,
748736 & parsed. inserts ,
749737 s_alias,
750738 & injected_columns,
751- & table_fields ,
739+ table . schema ( ) . fields ( ) ,
752740 & mut temp_tracker,
753741 )
754742 . await ?;
@@ -854,7 +842,7 @@ async fn build_insert_batches(
854842 inserts : & [ MergeInsertClause ] ,
855843 s_alias : & str ,
856844 injected_columns : & [ String ] ,
857- table_fields : & [ String ] ,
845+ table_fields : & [ DataField ] ,
858846 temp_tracker : & mut TempTableTracker < ' _ > ,
859847) -> DFResult < Vec < RecordBatch > > {
860848 if not_matched_batches. is_empty ( ) || not_matched_batches. iter ( ) . all ( |b| b. num_rows ( ) == 0 ) {
@@ -883,7 +871,7 @@ async fn build_insert_batches_inner(
883871 inserts : & [ MergeInsertClause ] ,
884872 s_alias : & str ,
885873 tmp_name : & str ,
886- table_fields : & [ String ] ,
874+ table_fields : & [ DataField ] ,
887875) -> DFResult < Vec < RecordBatch > > {
888876 let mut all_batches = Vec :: new ( ) ;
889877 let mut consumed_predicates: Vec < String > = Vec :: new ( ) ;
@@ -910,12 +898,61 @@ async fn build_insert_batches_inner(
910898 let sql = format ! ( "SELECT {select_clause} FROM {tmp_name} AS {s_alias}{where_clause}" ) ;
911899
912900 let batches = ctx. ctx ( ) . sql ( & sql) . await ?. collect ( ) . await ?;
913- all_batches. extend ( batches) ;
901+ for batch in batches {
902+ all_batches. push ( normalize_insert_batch_to_table_schema (
903+ & batch,
904+ table_fields,
905+ ) ?) ;
906+ }
914907 }
915908
916909 Ok ( all_batches)
917910}
918911
912+ fn normalize_insert_batch_to_table_schema (
913+ batch : & RecordBatch ,
914+ table_fields : & [ DataField ] ,
915+ ) -> DFResult < RecordBatch > {
916+ if batch. num_columns ( ) != table_fields. len ( ) {
917+ return Err ( DataFusionError :: Plan ( format ! (
918+ "MERGE INSERT output has {} columns but target table has {}" ,
919+ batch. num_columns( ) ,
920+ table_fields. len( )
921+ ) ) ) ;
922+ }
923+
924+ let target_schema =
925+ paimon:: arrow:: build_target_arrow_schema ( table_fields) . map_err ( to_datafusion_error) ?;
926+ let mut columns = Vec :: with_capacity ( table_fields. len ( ) ) ;
927+
928+ for ( target_idx, field) in table_fields. iter ( ) . enumerate ( ) {
929+ let column = batch. column ( target_idx) . clone ( ) ;
930+ let target_type = target_schema. field ( target_idx) . data_type ( ) ;
931+ let column = cast_insert_column ( field. name ( ) , column, target_type) ?;
932+ columns. push ( column) ;
933+ }
934+
935+ RecordBatch :: try_new ( target_schema, columns) . map_err ( DataFusionError :: from)
936+ }
937+
938+ fn cast_insert_column (
939+ name : & str ,
940+ column : ArrayRef ,
941+ target_type : & ArrowDataType ,
942+ ) -> DFResult < ArrayRef > {
943+ if column. data_type ( ) == target_type {
944+ return Ok ( column) ;
945+ }
946+
947+ compute:: cast ( column. as_ref ( ) , target_type) . map_err ( |e| {
948+ DataFusionError :: Plan ( format ! (
949+ "Cannot cast MERGE INSERT column '{name}' from {:?} to {:?}: {e}" ,
950+ column. data_type( ) ,
951+ target_type
952+ ) )
953+ } )
954+ }
955+
919956/// Remove injected columns from batches, keeping only source columns.
920957fn strip_non_source_columns (
921958 batches : & [ RecordBatch ] ,
@@ -946,7 +983,7 @@ fn strip_non_source_columns(
946983/// When the INSERT specifies explicit columns (`INSERT (col2, col1) VALUES (expr2, expr1)`),
947984/// the output must be reordered to match the table schema so that `write_arrow_batch`
948985/// (which reads columns by positional index) maps them correctly.
949- fn insert_select_clause ( ins : & MergeInsertClause , table_fields : & [ String ] ) -> String {
986+ fn insert_select_clause ( ins : & MergeInsertClause , table_fields : & [ DataField ] ) -> String {
950987 if ins. columns . is_empty ( ) && ins. value_exprs . is_empty ( ) {
951988 "*" . to_string ( )
952989 } else {
@@ -962,11 +999,11 @@ fn insert_select_clause(ins: &MergeInsertClause, table_fields: &[String]) -> Str
962999 table_fields
9631000 . iter ( )
9641001 . map ( |field| {
965- let key = field. to_lowercase ( ) ;
1002+ let key = field. name ( ) . to_lowercase ( ) ;
9661003 match col_expr_map. get ( & key) {
967- Some ( expr) => format ! ( "{expr} AS {}" , quote_identifier( field) ) ,
1004+ Some ( expr) => format ! ( "{expr} AS {}" , quote_identifier( field. name ( ) ) ) ,
9681005 // Column not in INSERT list — fill with NULL
969- None => format ! ( "NULL AS {}" , quote_identifier( field) ) ,
1006+ None => format ! ( "NULL AS {}" , quote_identifier( field. name ( ) ) ) ,
9701007 }
9711008 } )
9721009 . collect :: < Vec < _ > > ( )
@@ -1546,7 +1583,7 @@ mod tests {
15461583 use datafusion:: sql:: sqlparser:: parser:: Parser ;
15471584 use paimon:: catalog:: { Catalog , Identifier } ;
15481585 use paimon:: io:: FileIOBuilder ;
1549- use paimon:: spec:: { DataType , IntType , Schema as PaimonSchema , TableSchema } ;
1586+ use paimon:: spec:: { DataField , DataType , IntType , Schema as PaimonSchema , TableSchema } ;
15501587 use paimon:: { CatalogOptions , FileSystemCatalog , Options } ;
15511588 use tempfile:: TempDir ;
15521589
@@ -1613,6 +1650,42 @@ mod tests {
16131650 }
16141651 }
16151652
1653+ #[ test]
1654+ fn test_normalize_merge_insert_batch_uses_position ( ) {
1655+ let table_fields = vec ! [
1656+ DataField :: new( 0 , "a" . to_string( ) , DataType :: Int ( IntType :: new( ) ) ) ,
1657+ DataField :: new( 1 , "b" . to_string( ) , DataType :: Int ( IntType :: new( ) ) ) ,
1658+ ] ;
1659+ let batch = RecordBatch :: try_new (
1660+ Arc :: new ( Schema :: new ( vec ! [
1661+ Field :: new( "b" , ArrowDataType :: Int32 , false ) ,
1662+ Field :: new( "x" , ArrowDataType :: Int32 , false ) ,
1663+ ] ) ) ,
1664+ vec ! [
1665+ Arc :: new( Int32Array :: from( vec![ 100 ] ) ) ,
1666+ Arc :: new( Int32Array :: from( vec![ 7 ] ) ) ,
1667+ ] ,
1668+ )
1669+ . unwrap ( ) ;
1670+
1671+ let normalized = normalize_insert_batch_to_table_schema ( & batch, & table_fields) . unwrap ( ) ;
1672+ let first = normalized
1673+ . column ( 0 )
1674+ . as_any ( )
1675+ . downcast_ref :: < Int32Array > ( )
1676+ . unwrap ( ) ;
1677+ let second = normalized
1678+ . column ( 1 )
1679+ . as_any ( )
1680+ . downcast_ref :: < Int32Array > ( )
1681+ . unwrap ( ) ;
1682+
1683+ assert_eq ! ( normalized. schema( ) . field( 0 ) . name( ) , "a" ) ;
1684+ assert_eq ! ( normalized. schema( ) . field( 1 ) . name( ) , "b" ) ;
1685+ assert_eq ! ( first. value( 0 ) , 100 ) ;
1686+ assert_eq ! ( second. value( 0 ) , 7 ) ;
1687+ }
1688+
16161689 #[ test]
16171690 fn test_source_partition_pruning_requires_partition_equality ( ) {
16181691 let merge = parse_merge (
0 commit comments