diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8a9a67a74..2afae7390 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -196,6 +196,36 @@ where DisplaySeparated { slice, sep: ", " } } +/// Returns true when the prefix operator `op` written directly before `operand` +/// would be tokenized as a single operator rather than as two. +fn lexes_as_one_operator(op: &UnaryOperator, operand: &Expr) -> bool { + let Expr::UnaryOp { op: leading, .. } = operand else { + return false; + }; + match op { + UnaryOperator::Minus + | UnaryOperator::BitwiseNot + | UnaryOperator::PGSquareRoot + | UnaryOperator::PGCubeRoot => !matches!( + leading, + UnaryOperator::Not | UnaryOperator::PGPostfixFactorial + ), + UnaryOperator::BangNot => { + matches!(leading, UnaryOperator::BangNot | UnaryOperator::BitwiseNot) + } + UnaryOperator::PGAbs => matches!( + leading, + UnaryOperator::AtDashAt + | UnaryOperator::DoubleAt + | UnaryOperator::Minus + | UnaryOperator::PGAbs + | UnaryOperator::QuestionDash + | UnaryOperator::QuestionPipe + ), + _ => false, + } +} + /// Writes the given statements to the formatter, each ending with /// a semicolon and space separated. fn format_statement_list(f: &mut fmt::Formatter, statements: &[Statement]) -> fmt::Result { @@ -1963,7 +1993,8 @@ impl fmt::Display for Expr { | UnaryOperator::DoubleAt | UnaryOperator::QuestionDash | UnaryOperator::QuestionPipe - ) { + ) || lexes_as_one_operator(op, expr) + { write!(f, "{op} {expr}") } else { write!(f, "{op}{expr}") diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0800bc41f..04ed3e913 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -19679,3 +19679,19 @@ fn parse_function_arg_call_chain_no_exponential_blowup() { rx.recv_timeout(Duration::from_secs(5)) .expect("parser should reject this quickly, not loop exponentially"); } + +#[test] +fn parse_nested_unary_ops() { + all_dialects().verified_stmt("SELECT - -1"); + all_dialects().verified_stmt("SELECT ~ ~1"); + all_dialects().verified_stmt("SELECT NOT NOT a"); + all_dialects().one_statement_parses_to("SELECT ~ ~ 1", "SELECT ~ ~1"); +} + +#[test] +fn parse_adjacent_unary_ops_that_do_not_combine() { + all_dialects().verified_stmt("SELECT ++a"); + all_dialects().verified_stmt("SELECT +~a"); + all_dialects().verified_stmt("SELECT -NOT a"); + all_dialects().verified_stmt("SELECT ~NOT a"); +} diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index a7128eafd..d0b76af1f 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -9663,3 +9663,37 @@ fn parse_right_deep_join_chain() { // NATURAL JOIN followed by a constrained join must stay left-associative. pg().verified_stmt("SELECT * FROM t0 NATURAL JOIN t1 INNER JOIN t2 ON true"); } + +#[test] +fn parse_nested_pg_unary_ops() { + let select = pg().verified_only_select("SELECT @ @1"); + assert_eq!( + SelectItem::UnnamedExpr(Expr::UnaryOp { + op: UnaryOperator::PGAbs, + expr: Box::new(Expr::UnaryOp { + op: UnaryOperator::PGAbs, + expr: Box::new(Expr::value(number("1"))), + }), + }), + select.projection[0] + ); + + let select = pg().verified_only_select("SELECT @@ 1"); + assert_eq!( + SelectItem::UnnamedExpr(Expr::UnaryOp { + op: UnaryOperator::DoubleAt, + expr: Box::new(Expr::value(number("1"))), + }), + select.projection[0] + ); + + pg().verified_stmt("SELECT |/ |/1"); +} + +#[test] +fn parse_adjacent_pg_unary_ops_that_do_not_combine() { + pg().verified_stmt("SELECT +@a"); + pg().verified_stmt("SELECT !!~a"); + pg().verified_stmt("SELECT @~a"); + pg().verified_stmt("SELECT !!|/a"); +}