@@ -528,9 +528,21 @@ pub fn detect_schema_conflicts(
528528
529529/// Renames fields in JSON values according to the provided field mapping.
530530/// Used to resolve schema conflicts by renaming fields with conflicting types.
531+ /// Renames conflicting JSON fields to typed sibling columns, but only for
532+ /// records whose value is incompatible with the existing field type.
533+ ///
534+ /// `field_mapping` is produced by `detect_schema_conflicts` and maps an
535+ /// original field name to its typed sibling name, e.g. `level -> level_utf8`.
536+ /// The mapping applies per value, not blindly to every record in the batch:
537+ /// if existing schema has `level: Float64`, then `{ "level": 30 }` stays as
538+ /// `level`, while `{ "level": "info" }` becomes `{ "level_utf8": "info" }`.
539+ /// This avoids routing compatible values into the sibling column and then
540+ /// failing validation, e.g. `level_utf8` expecting `Utf8` but receiving `30`.
531541pub fn rename_conflicting_fields_in_json (
532542 values : Vec < Value > ,
533543 field_mapping : & HashMap < String , String > ,
544+ existing_schema : & HashMap < String , Arc < Field > > ,
545+ schema_version : SchemaVersion ,
534546) -> Vec < Value > {
535547 if field_mapping. is_empty ( ) {
536548 return values;
@@ -544,7 +556,13 @@ pub fn rename_conflicting_fields_in_json(
544556 . into_iter ( )
545557 . map ( |( key, val) | {
546558 if let Some ( new_key) = field_mapping. get ( & key) {
547- ( new_key. clone ( ) , val)
559+ if existing_schema. get ( & key) . is_some_and ( |field| {
560+ value_compatible_with_type ( & val, field. data_type ( ) , schema_version)
561+ } ) {
562+ ( key, val)
563+ } else {
564+ ( new_key. clone ( ) , val)
565+ }
548566 } else {
549567 ( key, val)
550568 }
@@ -776,8 +794,8 @@ mod tests {
776794 #[ test]
777795 fn test_rename_conflicting_fields_in_json ( ) {
778796 let values = vec ! [
779- json!( { "body_timestamp" : "2025-01-01T00:00:00Z " , "message" : "hello" } ) ,
780- json!( { "body_timestamp" : "2025-01-02T00:00:00Z " , "message" : "world" } ) ,
797+ json!( { "body_timestamp" : "not a timestamp " , "message" : "hello" } ) ,
798+ json!( { "body_timestamp" : "also not a timestamp " , "message" : "world" } ) ,
781799 ] ;
782800
783801 let mut field_mapping = HashMap :: new ( ) ;
@@ -786,7 +804,22 @@ mod tests {
786804 "body_timestamp_timestamp_ms" . to_string ( ) ,
787805 ) ;
788806
789- let renamed = rename_conflicting_fields_in_json ( values, & field_mapping) ;
807+ let mut existing_schema: HashMap < String , Arc < Field > > = HashMap :: new ( ) ;
808+ existing_schema. insert (
809+ "body_timestamp" . to_string ( ) ,
810+ Arc :: new ( Field :: new (
811+ "body_timestamp" ,
812+ DataType :: Timestamp ( TimeUnit :: Millisecond , None ) ,
813+ true ,
814+ ) ) ,
815+ ) ;
816+
817+ let renamed = rename_conflicting_fields_in_json (
818+ values,
819+ & field_mapping,
820+ & existing_schema,
821+ SchemaVersion :: V1 ,
822+ ) ;
790823
791824 assert_eq ! ( renamed. len( ) , 2 ) ;
792825 assert ! ( renamed[ 0 ] . get( "body_timestamp_timestamp_ms" ) . is_some( ) ) ;
@@ -799,12 +832,44 @@ mod tests {
799832 let values = vec ! [ json!( { "body_timestamp" : "2025-01-01T00:00:00Z" } ) ] ;
800833
801834 let field_mapping = HashMap :: new ( ) ;
802- let renamed = rename_conflicting_fields_in_json ( values. clone ( ) , & field_mapping) ;
835+ let existing_schema = HashMap :: new ( ) ;
836+ let renamed = rename_conflicting_fields_in_json (
837+ values. clone ( ) ,
838+ & field_mapping,
839+ & existing_schema,
840+ SchemaVersion :: V1 ,
841+ ) ;
803842
804843 // Should return values unchanged
805844 assert_eq ! ( renamed, values) ;
806845 }
807846
847+ #[ test]
848+ fn test_rename_conflicting_fields_in_json_only_incompatible_records ( ) {
849+ let values = vec ! [ json!( { "level" : 30 } ) , json!( { "level" : "info" } ) ] ;
850+
851+ let mut field_mapping = HashMap :: new ( ) ;
852+ field_mapping. insert ( "level" . to_string ( ) , "level_utf8" . to_string ( ) ) ;
853+
854+ let mut existing_schema: HashMap < String , Arc < Field > > = HashMap :: new ( ) ;
855+ existing_schema. insert (
856+ "level" . to_string ( ) ,
857+ Arc :: new ( Field :: new ( "level" , DataType :: Float64 , true ) ) ,
858+ ) ;
859+
860+ let renamed = rename_conflicting_fields_in_json (
861+ values,
862+ & field_mapping,
863+ & existing_schema,
864+ SchemaVersion :: V1 ,
865+ ) ;
866+
867+ assert_eq ! ( renamed[ 0 ] . get( "level" ) , Some ( & json!( 30 ) ) ) ;
868+ assert ! ( renamed[ 0 ] . get( "level_utf8" ) . is_none( ) ) ;
869+ assert_eq ! ( renamed[ 1 ] . get( "level_utf8" ) , Some ( & json!( "info" ) ) ) ;
870+ assert ! ( renamed[ 1 ] . get( "level" ) . is_none( ) ) ;
871+ }
872+
808873 #[ test]
809874 fn test_detect_schema_conflicts_timestamp_vs_utf8 ( ) {
810875 // Existing schema has body_timestamp as Timestamp
0 commit comments