@@ -5190,8 +5190,47 @@ impl Unnest {
51905190
51915191 let metadata = input_schema. metadata ( ) . clone ( ) ;
51925192 let df_schema = DFSchema :: new_with_metadata ( fields, metadata) ?;
5193- // We can use the existing functional dependencies:
5194- let deps = input_schema. functional_dependencies ( ) . clone ( ) ;
5193+ let list_indices: HashSet < _ > =
5194+ list_columns. iter ( ) . map ( |( index, _) | * index) . collect ( ) ;
5195+ // Struct unnest may replace one input column with several output columns,
5196+ // so remap dependencies using the input index of each output column.
5197+ let remap_indices = |indices : & [ usize ] , exclude_lists : bool | {
5198+ dependency_indices
5199+ . iter ( )
5200+ . enumerate ( )
5201+ . filter_map ( |( output_index, input_index) | {
5202+ ( indices. contains ( input_index)
5203+ && ( !exclude_lists || !list_indices. contains ( input_index) ) )
5204+ . then_some ( output_index)
5205+ } )
5206+ . collect ( )
5207+ } ;
5208+ let deps = FunctionalDependencies :: new (
5209+ input_schema
5210+ . functional_dependencies ( )
5211+ . iter ( )
5212+ // A list input no longer determines the rows produced by unnest.
5213+ // A struct input is also unsafe as a source: a null struct and
5214+ // a non-null struct with all-null fields have the same outputs.
5215+ . filter ( |dep| {
5216+ dep. source_indices . iter ( ) . all ( |index| {
5217+ !list_indices. contains ( index) && !struct_columns. contains ( index)
5218+ } )
5219+ } )
5220+ . map ( |dep| FunctionalDependence {
5221+ source_indices : remap_indices ( & dep. source_indices , false ) ,
5222+ // The unnested values are not determined by the input key.
5223+ target_indices : remap_indices ( & dep. target_indices , true ) ,
5224+ // One input row can produce multiple output rows.
5225+ mode : if list_indices. is_empty ( ) {
5226+ dep. mode
5227+ } else {
5228+ Dependency :: Multi
5229+ } ,
5230+ ..dep. clone ( )
5231+ } )
5232+ . collect ( ) ,
5233+ ) ;
51955234 let schema = Arc :: new ( df_schema. with_functional_dependencies ( deps) ?) ;
51965235
51975236 Ok ( Unnest {
@@ -5297,7 +5336,7 @@ mod tests {
52975336 use datafusion_common:: tree_node:: {
52985337 TransformedResult , TreeNodeRewriter , TreeNodeVisitor ,
52995338 } ;
5300- use datafusion_common:: { Constraint , not_impl_err} ;
5339+ use datafusion_common:: { Constraint , RecursionUnnestOption , not_impl_err} ;
53015340 use insta:: { assert_debug_snapshot, assert_snapshot} ;
53025341 use std:: hash:: DefaultHasher ;
53035342
@@ -5377,6 +5416,80 @@ mod tests {
53775416 ] )
53785417 }
53795418
5419+ #[ test]
5420+ fn unnest_remaps_functional_dependencies ( ) -> Result < ( ) > {
5421+ let schema = Arc :: new ( Schema :: new ( vec ! [
5422+ Field :: new_struct(
5423+ "s" ,
5424+ vec![
5425+ Field :: new( "a" , DataType :: Int32 , true ) ,
5426+ Field :: new( "b" , DataType :: Int32 , true ) ,
5427+ ] ,
5428+ true ,
5429+ ) ,
5430+ Field :: new( "k" , DataType :: Int32 , false ) ,
5431+ Field :: new_list(
5432+ "xs" ,
5433+ Field :: new_list(
5434+ "item" ,
5435+ Field :: new_list_field( DataType :: Int32 , true ) ,
5436+ true ,
5437+ ) ,
5438+ true ,
5439+ ) ,
5440+ Field :: new( "payload" , DataType :: Utf8 , true ) ,
5441+ ] ) ) ;
5442+ let constraints = Constraints :: new_unverified ( vec ! [
5443+ Constraint :: PrimaryKey ( vec![ 1 ] ) ,
5444+ Constraint :: Unique ( vec![ 0 ] ) ,
5445+ Constraint :: Unique ( vec![ 2 ] ) ,
5446+ ] ) ;
5447+ let source: Arc < dyn TableSource > =
5448+ Arc :: new ( LogicalTableSource :: new ( schema) . with_constraints ( constraints) ) ;
5449+
5450+ // Expanding a struct preserves row count and remaps keys and targets,
5451+ // but a dependency sourced from the struct itself cannot be kept.
5452+ let struct_only = LogicalPlanBuilder :: scan ( "t" , Arc :: clone ( & source) , None ) ?
5453+ . unnest_column ( "s" ) ?
5454+ . build ( ) ?;
5455+ assert_eq ! (
5456+ struct_only. schema( ) . functional_dependencies( ) ,
5457+ & FunctionalDependencies :: new( vec![
5458+ FunctionalDependence :: new( vec![ 2 ] , vec![ 0 , 1 , 2 , 3 , 4 ] , false )
5459+ . with_mode( Dependency :: Single ) ,
5460+ FunctionalDependence :: new( vec![ 3 ] , vec![ 0 , 1 , 2 , 3 , 4 ] , true )
5461+ . with_mode( Dependency :: Single ) ,
5462+ ] )
5463+ ) ;
5464+
5465+ // The two recursion outputs both replace the same list input. Neither
5466+ // is determined by k, and a list-sourced dependency is no longer valid.
5467+ let options = UnnestOptions :: default ( )
5468+ . with_recursions ( RecursionUnnestOption {
5469+ input_column : "xs" . into ( ) ,
5470+ output_column : "xs_depth_1" . into ( ) ,
5471+ depth : 1 ,
5472+ } )
5473+ . with_recursions ( RecursionUnnestOption {
5474+ input_column : "xs" . into ( ) ,
5475+ output_column : "xs_depth_2" . into ( ) ,
5476+ depth : 2 ,
5477+ } ) ;
5478+ let plan = LogicalPlanBuilder :: scan ( "t" , source, None ) ?
5479+ . unnest_columns_with_options ( vec ! [ "s" . into( ) , "xs" . into( ) ] , options) ?
5480+ . build ( ) ?;
5481+ assert_eq ! ( plan. schema( ) . fields( ) . len( ) , 6 ) ;
5482+ assert_eq ! (
5483+ plan. schema( ) . functional_dependencies( ) ,
5484+ & FunctionalDependencies :: new( vec![
5485+ FunctionalDependence :: new( vec![ 2 ] , vec![ 0 , 1 , 2 , 5 ] , false , )
5486+ . with_mode( Dependency :: Multi )
5487+ ] )
5488+ ) ;
5489+
5490+ Ok ( ( ) )
5491+ }
5492+
53805493 #[ test]
53815494 fn projection_with_leading_computed_column_preserves_pk ( ) -> Result < ( ) > {
53825495 let constraints =
0 commit comments