@@ -111,6 +111,12 @@ pub(crate) struct VortexOpener {
111111
112112impl FileOpener for VortexOpener {
113113 fn open ( & self , file : PartitionedFile ) -> DFResult < FileOpenFuture > {
114+ // Calculate the output schema before replacing partition columns with literals so it
115+ // retains the table and partition-field metadata declared by the plan.
116+ let output_schema = Arc :: new (
117+ self . projection
118+ . project_schema ( self . table_schema . table_schema ( ) ) ?,
119+ ) ;
114120 let session = self . session . clone ( ) ;
115121 let metrics_registry = Arc :: clone ( & self . metrics_registry ) ;
116122 let labels = vec ! [
@@ -259,8 +265,6 @@ impl FileOpener for VortexOpener {
259265 & session. arrow ( ) ,
260266 ) ?) ;
261267
262- let projected_physical_schema = projection. project_schema ( & unified_file_schema) ?;
263-
264268 let expr_adapter = expr_adapter_factory. create (
265269 Arc :: clone ( & unified_file_schema) ,
266270 Arc :: clone ( & this_file_schema) ,
@@ -287,7 +291,7 @@ impl FileOpener for VortexOpener {
287291 expr_convertor. split_projection (
288292 projection. clone ( ) ,
289293 & this_file_schema,
290- & projected_physical_schema ,
294+ output_schema . as_ref ( ) ,
291295 ) ?
292296 } else {
293297 // When projection pushdown is disabled, read only the required columns
@@ -304,15 +308,15 @@ impl FileOpener for VortexOpener {
304308 // When projection pushdown is enabled, the scan outputs the projected columns.
305309 // When disabled, the scan outputs raw columns and the projection is applied after.
306310 let scan_reference_schema = if projection_pushdown {
307- projected_physical_schema
311+ ( * output_schema ) . clone ( )
308312 } else {
309313 // Build schema from the raw columns being read
310314 let column_indices = projection. column_indices ( ) ;
311315 let fields: Vec < _ > = column_indices
312316 . into_iter ( )
313317 . map ( |idx| this_file_schema. field ( idx) . clone ( ) )
314318 . collect ( ) ;
315- Schema :: new ( fields)
319+ Schema :: new_with_metadata ( fields, this_file_schema . metadata ( ) . clone ( ) )
316320 } ;
317321 let stream_schema =
318322 calculate_physical_schema ( & scan_dtype, & scan_reference_schema, & session. arrow ( ) ) ?;
@@ -450,11 +454,15 @@ impl FileOpener for VortexOpener {
450454 ) ) ) )
451455 } )
452456 . map ( move |batch| {
453- if projector. projection ( ) . as_ref ( ) . is_empty ( ) {
457+ let batch = if projector. projection ( ) . as_ref ( ) . is_empty ( ) {
454458 batch
455459 } else {
456460 batch. and_then ( |b| projector. project_batch ( & b) )
457- }
461+ } ?;
462+
463+ batch
464+ . with_schema ( Arc :: clone ( & output_schema) )
465+ . map_err ( Into :: into)
458466 } )
459467 . boxed ( ) ;
460468
@@ -814,6 +822,61 @@ mod tests {
814822 Ok ( ( ) )
815823 }
816824
825+ #[ tokio:: test]
826+ async fn test_open_preserves_declared_schema_metadata ( ) -> anyhow:: Result < ( ) > {
827+ let object_store = Arc :: new ( InMemory :: new ( ) ) as Arc < dyn ObjectStore > ;
828+ let file_path = "part=1/file.vortex" ;
829+ let batch = record_batch ! ( ( "a" , Int32 , vec![ Some ( 1 ) , Some ( 2 ) , Some ( 3 ) ] ) ) ?;
830+ let data_size =
831+ write_arrow_to_vortex ( Arc :: clone ( & object_store) , file_path, batch. clone ( ) ) . await ?;
832+
833+ let file_schema = Arc :: new (
834+ batch. schema ( ) . as_ref ( ) . clone ( ) . with_metadata (
835+ [ ( "table" . to_string ( ) , "metadata" . to_string ( ) ) ]
836+ . into_iter ( )
837+ . collect ( ) ,
838+ ) ,
839+ ) ;
840+ let table_schema = TableSchema :: new (
841+ file_schema,
842+ vec ! [ Arc :: new(
843+ Field :: new( "part" , DataType :: Int32 , false ) . with_metadata(
844+ [ ( "partition" . to_string( ) , "metadata" . to_string( ) ) ]
845+ . into_iter( )
846+ . collect( ) ,
847+ ) ,
848+ ) ] ,
849+ ) ;
850+ let projection = ProjectionExprs :: from_indices ( & [ 0 , 1 ] , table_schema. table_schema ( ) ) ;
851+ let expected_schema = Arc :: new ( projection. project_schema ( table_schema. table_schema ( ) ) ?) ;
852+
853+ assert_eq ! (
854+ expected_schema. metadata( ) . get( "table" ) ,
855+ Some ( & "metadata" . to_string( ) )
856+ ) ;
857+ assert_eq ! (
858+ expected_schema. field( 1 ) . metadata( ) . get( "partition" ) ,
859+ Some ( & "metadata" . to_string( ) )
860+ ) ;
861+
862+ for projection_pushdown in [ false , true ] {
863+ let mut opener = make_opener ( Arc :: clone ( & object_store) , table_schema. clone ( ) , None ) ;
864+ opener. projection = projection. clone ( ) ;
865+ opener. projection_pushdown = projection_pushdown;
866+
867+ let mut file = PartitionedFile :: new ( file_path. to_string ( ) , data_size) ;
868+ file. partition_values = vec ! [ ScalarValue :: Int32 ( Some ( 1 ) ) ] ;
869+ let batches = opener. open ( file) ?. await ?. try_collect :: < Vec < _ > > ( ) . await ?;
870+
871+ assert ! ( !batches. is_empty( ) ) ;
872+ for batch in batches {
873+ assert_eq ! ( batch. schema( ) . as_ref( ) , expected_schema. as_ref( ) ) ;
874+ }
875+ }
876+
877+ Ok ( ( ) )
878+ }
879+
817880 #[ tokio:: test]
818881 async fn test_file_pruning_replaces_partition_columns_without_file_statistics ( )
819882 -> anyhow:: Result < ( ) > {
0 commit comments