@@ -69,18 +69,43 @@ pub trait ExprSchemable {
6969 -> Result < ( DataType , bool ) > ;
7070}
7171
72- /// Derives the output field for a cast expression from the source field.
72+ /// Derives the output field for a cast expression from the source and target fields.
73+ ///
74+ /// Metadata handling:
75+ /// - Source field metadata is propagated by default
76+ /// - Extension type metadata keys (`ARROW:extension:name` and `ARROW:extension:metadata`)
77+ /// are taken from the target field, overwriting any extension metadata from the source.
78+ /// This ensures casting does not incorrectly propagate extension type identity.
79+ ///
7380/// For `TryCast`, `force_nullable` is `true` since a failed cast returns NULL.
7481fn cast_output_field (
7582 source_field : & FieldRef ,
76- target_type : & DataType ,
83+ target_field : & FieldRef ,
7784 force_nullable : bool ,
7885) -> Arc < Field > {
86+ use arrow_schema:: extension:: { EXTENSION_TYPE_METADATA_KEY , EXTENSION_TYPE_NAME_KEY } ;
87+
88+ // Start with source metadata
89+ let mut metadata = source_field. metadata ( ) . clone ( ) ;
90+
91+ // Remove any extension type metadata from source - these should not propagate through casts
92+ metadata. remove ( EXTENSION_TYPE_NAME_KEY ) ;
93+ metadata. remove ( EXTENSION_TYPE_METADATA_KEY ) ;
94+
95+ // Add extension type metadata from the target field if present
96+ let target_metadata = target_field. metadata ( ) ;
97+ if let Some ( name) = target_metadata. get ( EXTENSION_TYPE_NAME_KEY ) {
98+ metadata. insert ( EXTENSION_TYPE_NAME_KEY . to_string ( ) , name. clone ( ) ) ;
99+ }
100+ if let Some ( ext_meta) = target_metadata. get ( EXTENSION_TYPE_METADATA_KEY ) {
101+ metadata. insert ( EXTENSION_TYPE_METADATA_KEY . to_string ( ) , ext_meta. clone ( ) ) ;
102+ }
103+
79104 let mut f = source_field
80105 . as_ref ( )
81106 . clone ( )
82- . with_data_type ( target_type . clone ( ) )
83- . with_metadata ( source_field . metadata ( ) . clone ( ) ) ;
107+ . with_data_type ( target_field . data_type ( ) . clone ( ) )
108+ . with_metadata ( metadata) ;
84109 if force_nullable {
85110 f = f. with_nullable ( true ) ;
86111 }
@@ -595,20 +620,16 @@ impl ExprSchemable for Expr {
595620 func. return_field_from_args ( args)
596621 }
597622 // _ => Ok((self.get_type(schema)?, self.nullable(schema)?)),
598- Expr :: Cast ( Cast { expr, field } ) => {
599- expr. to_field ( schema) . map ( |( _table_ref, src) | {
600- cast_output_field ( & src, field. data_type ( ) , false )
601- } )
602- }
623+ Expr :: Cast ( Cast { expr, field } ) => expr
624+ . to_field ( schema)
625+ . map ( |( _table_ref, src) | cast_output_field ( & src, field, false ) ) ,
603626 Expr :: Placeholder ( Placeholder {
604627 id : _,
605628 field : Some ( field) ,
606629 } ) => Ok ( Arc :: clone ( field) . renamed ( & schema_name) ) ,
607- Expr :: TryCast ( TryCast { expr, field } ) => {
608- expr. to_field ( schema) . map ( |( _table_ref, src) | {
609- cast_output_field ( & src, field. data_type ( ) , true )
610- } )
611- }
630+ Expr :: TryCast ( TryCast { expr, field } ) => expr
631+ . to_field ( schema)
632+ . map ( |( _table_ref, src) | cast_output_field ( & src, field, true ) ) ,
612633 Expr :: LambdaVariable ( LambdaVariable {
613634 field : Some ( field) , ..
614635 } ) => Ok ( Arc :: clone ( field) . renamed ( & schema_name) ) ,
@@ -1208,4 +1229,121 @@ mod tests {
12081229
12091230 assert_eq ! ( meta, expr. metadata( & schema) . unwrap( ) ) ;
12101231 }
1232+
1233+ #[ test]
1234+ fn test_cast_extension_type_metadata ( ) {
1235+ use crate :: expr:: Cast ;
1236+ use arrow_schema:: extension:: {
1237+ EXTENSION_TYPE_METADATA_KEY , EXTENSION_TYPE_NAME_KEY ,
1238+ } ;
1239+
1240+ // Create a schema with a field that has extension type metadata
1241+ let mut source_meta = HashMap :: new ( ) ;
1242+ source_meta. insert (
1243+ EXTENSION_TYPE_NAME_KEY . to_string ( ) ,
1244+ "arrow.uuid" . to_string ( ) ,
1245+ ) ;
1246+ source_meta. insert ( "custom_key" . to_string ( ) , "custom_value" . to_string ( ) ) ;
1247+
1248+ let source_field = Field :: new ( "foo" , DataType :: FixedSizeBinary ( 16 ) , false )
1249+ . with_metadata ( source_meta) ;
1250+
1251+ let schema = MockExprSchema :: new ( )
1252+ . with_data_type ( DataType :: FixedSizeBinary ( 16 ) )
1253+ . with_metadata ( FieldMetadata :: from ( source_field. metadata ( ) . clone ( ) ) ) ;
1254+
1255+ // Test 1: Cast to a type without extension metadata strips extension metadata
1256+ // but preserves non-extension metadata
1257+ let cast_expr = Expr :: Cast ( Cast {
1258+ expr : Box :: new ( col ( "foo" ) ) ,
1259+ field : Arc :: new ( Field :: new ( "" , DataType :: Utf8 , true ) ) ,
1260+ } ) ;
1261+
1262+ let ( _, result_field) = cast_expr. to_field ( & schema) . unwrap ( ) ;
1263+ assert ! (
1264+ result_field
1265+ . metadata( )
1266+ . get( EXTENSION_TYPE_NAME_KEY )
1267+ . is_none( ) ,
1268+ "Extension type name should be stripped when target has no extension metadata"
1269+ ) ;
1270+ assert_eq ! (
1271+ result_field. metadata( ) . get( "custom_key" ) ,
1272+ Some ( & "custom_value" . to_string( ) ) ,
1273+ "Non-extension metadata should be preserved"
1274+ ) ;
1275+
1276+ // Test 2: Cast to a field with different extension type replaces extension metadata
1277+ let mut target_meta = HashMap :: new ( ) ;
1278+ target_meta. insert (
1279+ EXTENSION_TYPE_NAME_KEY . to_string ( ) ,
1280+ "arrow.json" . to_string ( ) ,
1281+ ) ;
1282+ target_meta. insert ( EXTENSION_TYPE_METADATA_KEY . to_string ( ) , "{}" . to_string ( ) ) ;
1283+
1284+ let target_field =
1285+ Field :: new ( "" , DataType :: Utf8 , true ) . with_metadata ( target_meta) ;
1286+
1287+ let cast_expr = Expr :: Cast ( Cast {
1288+ expr : Box :: new ( col ( "foo" ) ) ,
1289+ field : Arc :: new ( target_field) ,
1290+ } ) ;
1291+
1292+ let ( _, result_field) = cast_expr. to_field ( & schema) . unwrap ( ) ;
1293+ assert_eq ! (
1294+ result_field. metadata( ) . get( EXTENSION_TYPE_NAME_KEY ) ,
1295+ Some ( & "arrow.json" . to_string( ) ) ,
1296+ "Extension type name should come from target field"
1297+ ) ;
1298+ assert_eq ! (
1299+ result_field. metadata( ) . get( EXTENSION_TYPE_METADATA_KEY ) ,
1300+ Some ( & "{}" . to_string( ) ) ,
1301+ "Extension type metadata should come from target field"
1302+ ) ;
1303+ assert_eq ! (
1304+ result_field. metadata( ) . get( "custom_key" ) ,
1305+ Some ( & "custom_value" . to_string( ) ) ,
1306+ "Non-extension metadata should still be preserved from source"
1307+ ) ;
1308+ }
1309+
1310+ #[ test]
1311+ fn test_try_cast_extension_type_metadata ( ) {
1312+ use crate :: expr:: TryCast ;
1313+ use arrow_schema:: extension:: EXTENSION_TYPE_NAME_KEY ;
1314+
1315+ // Create a schema with a field that has extension type metadata
1316+ let mut source_meta = HashMap :: new ( ) ;
1317+ source_meta. insert (
1318+ EXTENSION_TYPE_NAME_KEY . to_string ( ) ,
1319+ "arrow.uuid" . to_string ( ) ,
1320+ ) ;
1321+
1322+ let source_field = Field :: new ( "foo" , DataType :: FixedSizeBinary ( 16 ) , false )
1323+ . with_metadata ( source_meta) ;
1324+
1325+ let schema = MockExprSchema :: new ( )
1326+ . with_data_type ( DataType :: FixedSizeBinary ( 16 ) )
1327+ . with_metadata ( FieldMetadata :: from ( source_field. metadata ( ) . clone ( ) ) ) ;
1328+
1329+ // TryCast should also strip extension metadata when target has none
1330+ let try_cast_expr = Expr :: TryCast ( TryCast {
1331+ expr : Box :: new ( col ( "foo" ) ) ,
1332+ field : Arc :: new ( Field :: new ( "" , DataType :: Utf8 , true ) ) ,
1333+ } ) ;
1334+
1335+ let ( _, result_field) = try_cast_expr. to_field ( & schema) . unwrap ( ) ;
1336+ assert ! (
1337+ result_field
1338+ . metadata( )
1339+ . get( EXTENSION_TYPE_NAME_KEY )
1340+ . is_none( ) ,
1341+ "Extension type name should be stripped in TryCast"
1342+ ) ;
1343+ // TryCast should force nullable
1344+ assert ! (
1345+ result_field. is_nullable( ) ,
1346+ "TryCast result should be nullable"
1347+ ) ;
1348+ }
12111349}
0 commit comments