@@ -2,7 +2,7 @@ use std::sync::Arc;
22
33use bigdecimal:: { num_bigint:: BigInt , BigDecimal } ;
44use datafusion:: {
5- logical_expr:: { Cast , Expr } ,
5+ logical_expr:: { Cast , Expr , Operator } ,
66 scalar:: ScalarValue ,
77 sql:: unparser:: dialect:: {
88 DefaultDialect , Dialect , DuckDBDialect , MySqlDialect , PostgreSqlDialect , SqliteDialect ,
@@ -64,7 +64,12 @@ pub fn to_sql_with_engine(expr: &Expr, engine: Option<Engine>) -> Result<String>
6464 }
6565 }
6666
67- Ok ( format ! ( "{} {} {}" , left, binary_expr. op, right) )
67+ match binary_expr. op {
68+ Operator :: And | Operator :: Or => {
69+ Ok ( format ! ( "({}) {} ({})" , left, binary_expr. op, right) )
70+ }
71+ _ => Ok ( format ! ( "{} {} {}" , left, binary_expr. op, right) ) ,
72+ }
6873 }
6974 Expr :: Column ( name) => match engine {
7075 Some ( Engine :: Spark | Engine :: ODBC ) => Ok ( format ! ( "{name}" ) ) ,
@@ -536,6 +541,74 @@ mod tests {
536541 Ok ( ( ) )
537542 }
538543
544+ fn binary ( left : Expr , op : Operator , right : Expr ) -> Expr {
545+ Expr :: BinaryExpr ( datafusion:: logical_expr:: BinaryExpr {
546+ left : Box :: new ( left) ,
547+ op,
548+ right : Box :: new ( right) ,
549+ } )
550+ }
551+
552+ fn int ( v : i32 ) -> Expr {
553+ Expr :: Literal ( ScalarValue :: Int32 ( Some ( v) ) , None )
554+ }
555+
556+ #[ test]
557+ fn test_and_or_binary_exprs_are_parenthesized ( ) -> Result < ( ) > {
558+ // AND operands are wrapped so the tree shape is preserved in SQL
559+ let and_expr = binary ( col ( "a" ) , Operator :: And , col ( "b" ) ) ;
560+ assert_eq ! ( to_sql( & and_expr) ?, "(\" a\" ) AND (\" b\" )" ) ;
561+
562+ // OR operands are wrapped for the same reason
563+ let or_expr = binary ( col ( "a" ) , Operator :: Or , col ( "b" ) ) ;
564+ assert_eq ! ( to_sql( & or_expr) ?, "(\" a\" ) OR (\" b\" )" ) ;
565+
566+ Ok ( ( ) )
567+ }
568+
569+ #[ test]
570+ fn test_non_boolean_binary_exprs_not_parenthesized ( ) -> Result < ( ) > {
571+ // Comparison and arithmetic operators must NOT add extra parens
572+ let eq_expr = binary ( col ( "k1" ) , Operator :: Eq , int ( 1 ) ) ;
573+ assert_eq ! ( to_sql( & eq_expr) ?, "\" k1\" = 1" ) ;
574+
575+ let lt_expr = binary ( col ( "v" ) , Operator :: Lt , int ( 42 ) ) ;
576+ assert_eq ! ( to_sql( & lt_expr) ?, "\" v\" < 42" ) ;
577+
578+ let plus_expr = binary ( col ( "x" ) , Operator :: Plus , int ( 5 ) ) ;
579+ assert_eq ! ( to_sql( & plus_expr) ?, "\" x\" + 5" ) ;
580+
581+ Ok ( ( ) )
582+ }
583+
584+ #[ test]
585+ fn test_composite_key_or_chain_parenthesized ( ) -> Result < ( ) > {
586+ // Simulates a two-row composite-key DELETE:
587+ // (k1 = 1 AND k2 = 2) OR (k1 = 2 AND k2 = 4)
588+ //
589+ // Without parens this serialises as a flat chain that DuckDB's optimizer
590+ // reconstructs as a left-recursive tree of depth N, causing a stack overflow
591+ // for large N. With parens the structure is explicit.
592+ let row1 = binary (
593+ binary ( col ( "k1" ) , Operator :: Eq , int ( 1 ) ) ,
594+ Operator :: And ,
595+ binary ( col ( "k2" ) , Operator :: Eq , int ( 2 ) ) ,
596+ ) ;
597+ let row2 = binary (
598+ binary ( col ( "k1" ) , Operator :: Eq , int ( 2 ) ) ,
599+ Operator :: And ,
600+ binary ( col ( "k2" ) , Operator :: Eq , int ( 4 ) ) ,
601+ ) ;
602+ let or_chain = binary ( row1, Operator :: Or , row2) ;
603+
604+ assert_eq ! (
605+ to_sql( & or_chain) ?,
606+ "((\" k1\" = 1) AND (\" k2\" = 2)) OR ((\" k1\" = 2) AND (\" k2\" = 4))"
607+ ) ;
608+
609+ Ok ( ( ) )
610+ }
611+
539612 #[ test]
540613 fn test_expr_timestamp_scalar_value_to_sql ( ) -> Result < ( ) > {
541614 let expr = Expr :: Literal (
0 commit comments