@@ -233,22 +233,11 @@ impl SpjNormalForm {
233233 . map ( |expr| predicate. normalize_expr ( expr) )
234234 . collect ( ) ;
235235
236- let mut referenced_tables = vec ! [ ] ;
237- original_plan
238- . apply ( |plan| {
239- if let LogicalPlan :: TableScan ( scan) = plan {
240- referenced_tables. push ( scan. table_name . clone ( ) ) ;
241- }
242-
243- Ok ( TreeNodeRecursion :: Continue )
244- } )
245- // No chance of error since we never return Err -- this unwrap is safe
246- . unwrap ( ) ;
247-
248236 Ok ( Self {
249237 output_schema : Arc :: clone ( original_plan. schema ( ) ) ,
250238 output_exprs,
251- referenced_tables,
239+ // Reuse referenced_tables collected during Predicate::new to avoid extra traversal
240+ referenced_tables : predicate. referenced_tables . clone ( ) ,
252241 predicate,
253242 } )
254243 }
@@ -344,84 +333,95 @@ struct Predicate {
344333 ranges_by_equivalence_class : Vec < Option < Interval > > ,
345334 /// Filter expressions that aren't column equality predicates or range filters.
346335 residuals : HashSet < Expr > ,
336+ /// Tables referenced in this plan (collected during single-pass traversal)
337+ referenced_tables : Vec < TableReference > ,
347338}
348339
349340impl Predicate {
341+ /// Create a new Predicate by analyzing the given logical plan.
342+ /// Uses single-pass traversal to collect schema, columns, filters, and referenced tables.
350343 fn new ( plan : & LogicalPlan ) -> Result < Self > {
351344 let mut schema = DFSchema :: empty ( ) ;
352- plan. apply ( |plan| {
353- if let LogicalPlan :: TableScan ( scan) = plan {
354- let new_schema = DFSchema :: try_from_qualified_schema (
355- scan. table_name . clone ( ) ,
356- scan. source . schema ( ) . as_ref ( ) ,
357- ) ?;
358- schema = if schema. fields ( ) . is_empty ( ) {
359- new_schema
360- } else {
361- schema. join ( & new_schema) ?
362- }
363- }
345+ let mut columns_info: Vec < ( Column , arrow:: datatypes:: DataType ) > = Vec :: new ( ) ;
346+ let mut filters: Vec < Expr > = Vec :: new ( ) ;
347+ let mut referenced_tables: Vec < TableReference > = Vec :: new ( ) ;
348+
349+ // Single traversal to collect everything
350+ plan. apply ( |node| {
351+ match node {
352+ LogicalPlan :: TableScan ( scan) => {
353+ // Collect referenced table
354+ referenced_tables. push ( scan. table_name . clone ( ) ) ;
364355
365- Ok ( TreeNodeRecursion :: Continue )
366- } ) ?;
356+ // Build schema
357+ let new_schema = DFSchema :: try_from_qualified_schema (
358+ scan. table_name . clone ( ) ,
359+ scan. source . schema ( ) . as_ref ( ) ,
360+ ) ?;
361+
362+ // Collect columns with their data types
363+ for ( table_ref, field) in new_schema. iter ( ) {
364+ columns_info. push ( (
365+ Column :: new ( table_ref. cloned ( ) , field. name ( ) ) ,
366+ field. data_type ( ) . clone ( ) ,
367+ ) ) ;
368+ }
367369
368- let mut new = Self {
369- schema,
370- eq_classes : vec ! [ ] ,
371- eq_class_idx_by_column : HashMap :: default ( ) ,
372- ranges_by_equivalence_class : vec ! [ ] ,
373- residuals : HashSet :: new ( ) ,
374- } ;
370+ // Merge schema
371+ schema = if schema. fields ( ) . is_empty ( ) {
372+ new_schema
373+ } else {
374+ schema. join ( & new_schema) ?
375+ } ;
375376
376- // Collect all referenced columns
377- plan. apply ( |plan| {
378- if let LogicalPlan :: TableScan ( scan) = plan {
379- for ( i, ( table_ref, field) ) in DFSchema :: try_from_qualified_schema (
380- scan. table_name . clone ( ) ,
381- scan. source . schema ( ) . as_ref ( ) ,
382- ) ?
383- . iter ( )
384- . enumerate ( )
385- {
386- let column = Column :: new ( table_ref. cloned ( ) , field. name ( ) ) ;
387- let data_type = field. data_type ( ) ;
388- new. eq_classes
389- . push ( ColumnEquivalenceClass :: new_singleton ( column. clone ( ) ) ) ;
390- new. eq_class_idx_by_column . insert ( column, i) ;
391- new. ranges_by_equivalence_class
392- . push ( Some ( Interval :: make_unbounded ( data_type) ?) ) ;
377+ // Collect filters from TableScan
378+ filters. extend ( scan. filters . iter ( ) . cloned ( ) ) ;
379+ }
380+ LogicalPlan :: Filter ( filter) => {
381+ filters. push ( filter. predicate . clone ( ) ) ;
393382 }
394- }
395-
396- Ok ( TreeNodeRecursion :: Continue )
397- } ) ?;
398-
399- // Collect any filters
400- plan. apply ( |plan| {
401- let filters = match plan {
402- LogicalPlan :: TableScan ( scan) => scan. filters . as_slice ( ) ,
403- LogicalPlan :: Filter ( filter) => core:: slice:: from_ref ( & filter. predicate ) ,
404383 LogicalPlan :: Join ( _join) => {
405384 return Err ( DataFusionError :: Internal (
406385 "joins are not supported yet" . to_string ( ) ,
407- ) )
386+ ) ) ;
408387 }
409- LogicalPlan :: Projection ( _) => & [ ] ,
388+ LogicalPlan :: Projection ( _) => { }
410389 _ => {
411390 return Err ( DataFusionError :: Plan ( format ! (
412391 "unsupported logical plan: {}" ,
413- plan . display( )
414- ) ) )
392+ node . display( )
393+ ) ) ) ;
415394 }
416- } ;
417-
418- for expr in filters. iter ( ) . flat_map ( split_conjunction) {
419- new. insert_conjuct ( expr) ?;
420395 }
421-
422396 Ok ( TreeNodeRecursion :: Continue )
423397 } ) ?;
424398
399+ // Initialize data structures with known capacity
400+ let num_columns = columns_info. len ( ) ;
401+ let mut eq_classes = Vec :: with_capacity ( num_columns) ;
402+ let mut eq_class_idx_by_column = HashMap :: with_capacity ( num_columns) ;
403+ let mut ranges_by_equivalence_class = Vec :: with_capacity ( num_columns) ;
404+
405+ for ( i, ( column, data_type) ) in columns_info. into_iter ( ) . enumerate ( ) {
406+ eq_classes. push ( ColumnEquivalenceClass :: new_singleton ( column. clone ( ) ) ) ;
407+ eq_class_idx_by_column. insert ( column, i) ;
408+ ranges_by_equivalence_class. push ( Some ( Interval :: make_unbounded ( & data_type) ?) ) ;
409+ }
410+
411+ let mut new = Self {
412+ schema,
413+ eq_classes,
414+ eq_class_idx_by_column,
415+ ranges_by_equivalence_class,
416+ residuals : HashSet :: new ( ) ,
417+ referenced_tables,
418+ } ;
419+
420+ // Process all collected filters
421+ for expr in filters. iter ( ) . flat_map ( split_conjunction) {
422+ new. insert_conjuct ( expr) ?;
423+ }
424+
425425 Ok ( new)
426426 }
427427
@@ -1163,11 +1163,11 @@ mod test {
11631163 TestCase {
11641164 name: "range filter + equality predicate" ,
11651165 base:
1166- "SELECT column1, column2 FROM t1 WHERE column1 = column3 AND column1 >= '2022'" ,
1166+ "SELECT column1, column2 FROM t1 WHERE column1 = column3 AND column1 >= '2022'" ,
11671167 query:
11681168 // Since column1 = column3 in the original view,
11691169 // we are allowed to substitute column1 for column3 and vice versa.
1170- "SELECT column2, column3 FROM t1 WHERE column1 = column3 AND column3 >= '2023'" ,
1170+ "SELECT column2, column3 FROM t1 WHERE column1 = column3 AND column3 >= '2023'" ,
11711171 } ,
11721172 TestCase {
11731173 name: "range filter with inequality on non-discrete type" ,
@@ -1229,4 +1229,95 @@ mod test {
12291229
12301230 Ok ( ( ) )
12311231 }
1232+
1233+ #[ tokio:: test]
1234+ async fn test_predicate_new_collects_expected_data ( ) -> Result < ( ) > {
1235+ let ctx = SessionContext :: new ( ) ;
1236+
1237+ // Create a table with known schema
1238+ ctx. sql (
1239+ "CREATE TABLE test_table (
1240+ col1 INT,
1241+ col2 VARCHAR,
1242+ col3 DOUBLE
1243+ )" ,
1244+ )
1245+ . await ?
1246+ . collect ( )
1247+ . await ?;
1248+
1249+ // Create a plan with filters
1250+ let plan = ctx
1251+ . sql ( "SELECT col1, col2 FROM test_table WHERE col1 >= 10 AND col2 = col3" )
1252+ . await ?
1253+ . into_optimized_plan ( ) ?;
1254+
1255+ let normal_form = SpjNormalForm :: new ( & plan) ?;
1256+
1257+ // Verify referenced_tables is collected
1258+ assert_eq ! ( normal_form. referenced_tables( ) . len( ) , 1 ) ;
1259+ assert_eq ! ( normal_form. referenced_tables( ) [ 0 ] . to_string( ) , "test_table" ) ;
1260+
1261+ // Verify output_exprs matches the projection (2 columns)
1262+ assert_eq ! ( normal_form. output_exprs( ) . len( ) , 2 ) ;
1263+
1264+ // Verify schema is preserved
1265+ assert_eq ! ( normal_form. output_schema( ) . fields( ) . len( ) , 2 ) ;
1266+
1267+ Ok ( ( ) )
1268+ }
1269+
1270+ #[ tokio:: test]
1271+ async fn test_predicate_new_with_join_returns_error ( ) -> Result < ( ) > {
1272+ let ctx = SessionContext :: new ( ) ;
1273+
1274+ ctx. sql ( "CREATE TABLE t1 (a INT, b INT)" )
1275+ . await ?
1276+ . collect ( )
1277+ . await ?;
1278+ ctx. sql ( "CREATE TABLE t2 (c INT, d INT)" )
1279+ . await ?
1280+ . collect ( )
1281+ . await ?;
1282+
1283+ // Test that join returns an error as it's not supported yet
1284+ let plan = ctx
1285+ . sql ( "SELECT t1.a, t2.d FROM t1 JOIN t2 ON t1.b = t2.c WHERE t1.a >= 0 AND t2.d <= 100" )
1286+ . await ?
1287+ . into_optimized_plan ( ) ?;
1288+
1289+ let result = SpjNormalForm :: new ( & plan) ;
1290+
1291+ // Verify that join returns an error
1292+ assert ! ( result. is_err( ) ) ;
1293+ assert ! ( result
1294+ . unwrap_err( )
1295+ . to_string( )
1296+ . contains( "joins are not supported yet" ) ) ;
1297+
1298+ Ok ( ( ) )
1299+ }
1300+
1301+ #[ tokio:: test]
1302+ async fn test_predicate_new_with_range_filters ( ) -> Result < ( ) > {
1303+ let ctx = SessionContext :: new ( ) ;
1304+
1305+ ctx. sql ( "CREATE TABLE range_test (x INT, y INT, z VARCHAR)" )
1306+ . await ?
1307+ . collect ( )
1308+ . await ?;
1309+
1310+ let plan = ctx
1311+ . sql ( "SELECT * FROM range_test WHERE x >= 10 AND x <= 100 AND y = 50" )
1312+ . await ?
1313+ . into_optimized_plan ( ) ?;
1314+
1315+ let normal_form = SpjNormalForm :: new ( & plan) ?;
1316+
1317+ // Verify all columns are in output
1318+ assert_eq ! ( normal_form. output_exprs( ) . len( ) , 3 ) ;
1319+ assert_eq ! ( normal_form. referenced_tables( ) . len( ) , 1 ) ;
1320+
1321+ Ok ( ( ) )
1322+ }
12321323}
0 commit comments