@@ -2,7 +2,6 @@ use arrow_pg::datatypes::df::encode_dataframe;
22use async_trait:: async_trait;
33use chrono:: Utc ;
44use datafusion:: common:: ScalarValue ;
5- use datafusion:: logical_expr:: logical_plan:: TableScan ;
65use datafusion:: logical_expr:: registry:: FunctionRegistry ;
76use datafusion:: logical_expr:: { LogicalPlan , LogicalPlanBuilder , col, lit} ;
87use datafusion:: prelude:: SessionContext ;
@@ -995,67 +994,6 @@ impl PolicyEffects {
995994
996995 tracing:: debug!( table = %scan. table_name, "PolicyHook: applying row filter" ) ;
997996
998- // Expand the TableScan projection to include any columns referenced by the
999- // filter expression. When projection=Some([]) (e.g. COUNT(*) optimisation),
1000- // the filter's columns would be missing from the scan's output schema without
1001- // this expansion, causing a schema mismatch at execution time.
1002- let expanded_projection: Option < Vec < usize > > =
1003- if let Some ( projection) = & scan. projection {
1004- let col_refs = filter_expr. column_refs ( ) ;
1005- if col_refs. is_empty ( ) {
1006- // lit(false) and similar zero-column-ref filters: no expansion needed.
1007- None
1008- } else {
1009- let full_schema = scan. source . schema ( ) ;
1010- let table_name_str = scan. table_name . to_string ( ) ;
1011- let mut extras: Vec < usize > = Vec :: new ( ) ;
1012- for col_ref in & col_refs {
1013- match full_schema. index_of ( & col_ref. name ) {
1014- Ok ( idx) if !projection. contains ( & idx) => extras. push ( idx) ,
1015- Ok ( _) => { } // already projected
1016- Err ( _) => {
1017- return Err ( datafusion:: error:: DataFusionError :: Plan (
1018- format ! (
1019- "Row filter references column '{}' not found in table '{table_name_str}'" ,
1020- col_ref. name
1021- ) ,
1022- ) ) ;
1023- }
1024- }
1025- }
1026- if extras. is_empty ( ) {
1027- None
1028- } else {
1029- let mut new_proj = projection. clone ( ) ;
1030- new_proj. extend ( extras) ;
1031- new_proj. sort_unstable ( ) ;
1032- Some ( new_proj)
1033- }
1034- }
1035- } else {
1036- // projection=None means all columns are already available.
1037- None
1038- } ;
1039-
1040- // Rebuild the TableScan with the expanded projection if needed.
1041- // (scan borrow ends above; NLL allows moving node here)
1042- let node = if let Some ( new_projection) = expanded_projection {
1043- let LogicalPlan :: TableScan ( scan_owned) = node else {
1044- unreachable ! ( )
1045- } ;
1046- let new_scan = TableScan :: try_new (
1047- scan_owned. table_name ,
1048- scan_owned. source ,
1049- Some ( new_projection) ,
1050- scan_owned. filters ,
1051- scan_owned. fetch ,
1052- )
1053- . map_err ( |e| datafusion:: error:: DataFusionError :: Plan ( e. to_string ( ) ) ) ?;
1054- LogicalPlan :: TableScan ( new_scan)
1055- } else {
1056- node
1057- } ;
1058-
1059997 let plan_with_filter = LogicalPlanBuilder :: from ( node)
1060998 . filter ( filter_expr. clone ( ) )
1061999 . and_then ( |b| b. build ( ) )
@@ -3023,121 +2961,4 @@ mod tests {
30232961 "orders.total remains: {col_names:?}"
30242962 ) ;
30252963 }
3026-
3027- // ---------- apply_row_filters projection expansion ----------
3028-
3029- /// Build a scan plan with an explicit column projection (indices into `columns`).
3030- fn build_scan_plan_with_projection (
3031- schema_table : & str ,
3032- columns : Vec < ( & str , DataType ) > ,
3033- projection : Option < Vec < usize > > ,
3034- ) -> LogicalPlan {
3035- let fields: Vec < Field > = columns
3036- . into_iter ( )
3037- . map ( |( name, dt) | Field :: new ( name, dt, true ) )
3038- . collect ( ) ;
3039- let schema = Arc :: new ( Schema :: new ( fields) ) ;
3040- let table = Arc :: new ( EmptyTable :: new ( schema) ) ;
3041- let source = Arc :: new ( DefaultTableSource :: new ( table) ) ;
3042- LogicalPlanBuilder :: scan ( schema_table, source, projection)
3043- . unwrap ( )
3044- . build ( )
3045- . unwrap ( )
3046- }
3047-
3048- fn make_effects_with_row_filter (
3049- key : ( & str , & str ) ,
3050- filter : datafusion:: logical_expr:: Expr ,
3051- ) -> PolicyEffects {
3052- let mut effects = PolicyEffects {
3053- row_filters : HashMap :: new ( ) ,
3054- column_allow_patterns : HashMap :: new ( ) ,
3055- column_deny_patterns : HashMap :: new ( ) ,
3056- column_masks : HashMap :: new ( ) ,
3057- tables_with_permit : HashSet :: new ( ) ,
3058- denied_by_policy : None ,
3059- } ;
3060- effects
3061- . row_filters
3062- . insert ( ( key. 0 . to_string ( ) , key. 1 . to_string ( ) ) , filter) ;
3063- effects
3064- }
3065-
3066- #[ test]
3067- fn test_row_filter_expands_narrow_projection ( ) {
3068- // TableScan projecting only col index 0 ("id"), filter references col index 1 ("tenant").
3069- // After expansion the projection must contain both 0 and 1.
3070- let effects = make_effects_with_row_filter ( ( "s1" , "orders" ) , col ( "tenant" ) . eq ( lit ( "acme" ) ) ) ;
3071-
3072- let plan = build_scan_plan_with_projection (
3073- "s1.orders" ,
3074- vec ! [
3075- ( "id" , DataType :: Int32 ) ,
3076- ( "tenant" , DataType :: Utf8 ) ,
3077- ( "amount" , DataType :: Int32 ) ,
3078- ] ,
3079- Some ( vec ! [ 0 ] ) , // only "id" projected
3080- ) ;
3081-
3082- let result = effects. apply_row_filters ( plan) . unwrap ( ) ;
3083-
3084- // The resulting plan must contain a Filter node above the TableScan.
3085- let display = format ! ( "{}" , result. display_indent( ) ) ;
3086- assert ! (
3087- display. contains( "Filter" ) ,
3088- "Expected Filter node in plan: {display}"
3089- ) ;
3090- // And the projected schema must include "tenant".
3091- let schema = result. schema ( ) ;
3092- let field_names: Vec < & str > = schema. fields ( ) . iter ( ) . map ( |f| f. name ( ) . as_str ( ) ) . collect ( ) ;
3093- assert ! (
3094- field_names. contains( & "tenant" ) ,
3095- "Expanded projection must include 'tenant': {field_names:?}"
3096- ) ;
3097- }
3098-
3099- #[ test]
3100- fn test_row_filter_no_expand_when_all_columns_present ( ) {
3101- // projection=None means all columns — no expansion should be attempted.
3102- let effects = make_effects_with_row_filter ( ( "s1" , "orders" ) , col ( "tenant" ) . eq ( lit ( "acme" ) ) ) ;
3103-
3104- // Build a scan with projection=None (all columns)
3105- let plan = build_scan_plan (
3106- "s1.orders" ,
3107- vec ! [ ( "id" , DataType :: Int32 ) , ( "tenant" , DataType :: Utf8 ) ] ,
3108- ) ;
3109-
3110- let result = effects. apply_row_filters ( plan) . unwrap ( ) ;
3111- let display = format ! ( "{}" , result. display_indent( ) ) ;
3112- assert ! (
3113- display. contains( "Filter" ) ,
3114- "Expected Filter node in plan: {display}"
3115- ) ;
3116- }
3117-
3118- #[ test]
3119- fn test_row_filter_lit_false_no_expand ( ) {
3120- // lit(false) has zero column refs — narrow projection should NOT be expanded.
3121- let effects = make_effects_with_row_filter ( ( "s1" , "orders" ) , lit ( false ) ) ;
3122-
3123- let plan = build_scan_plan_with_projection (
3124- "s1.orders" ,
3125- vec ! [ ( "id" , DataType :: Int32 ) , ( "tenant" , DataType :: Utf8 ) ] ,
3126- Some ( vec ! [ 0 ] ) , // only "id" projected — must NOT expand for lit(false)
3127- ) ;
3128-
3129- let result = effects. apply_row_filters ( plan) . unwrap ( ) ;
3130- let display = format ! ( "{}" , result. display_indent( ) ) ;
3131- assert ! (
3132- display. contains( "Filter" ) ,
3133- "Expected Filter node in plan: {display}"
3134- ) ;
3135- // "tenant" must NOT appear in the projected schema (no expansion for lit(false))
3136- let schema = result. schema ( ) ;
3137- let field_names: Vec < & str > = schema. fields ( ) . iter ( ) . map ( |f| f. name ( ) . as_str ( ) ) . collect ( ) ;
3138- assert ! (
3139- !field_names. contains( & "tenant" ) ,
3140- "lit(false) filter must not expand projection: {field_names:?}"
3141- ) ;
3142- }
31432964}
0 commit comments