diff --git a/fuzz/fuzz_seeds/postgres_2829_unary_minus_caret.sql b/fuzz/fuzz_seeds/postgres_2829_unary_minus_caret.sql new file mode 100644 index 0000000000..1d1b4ffa2f --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_2829_unary_minus_caret.sql @@ -0,0 +1 @@ +SELECT -1 ^ 0; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 22e3fa367a..03cd295d4b 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1152,7 +1152,7 @@ pub trait Dialect: Debug + Any { Precedence::Period => 100, Precedence::DoubleColon => 50, Precedence::AtTz => 41, - Precedence::MulDivModOp => 40, + Precedence::MulDivModOp | Precedence::UnaryPlusMinus => 40, Precedence::PlusMinus => 30, Precedence::Xor => 24, Precedence::Ampersand => 23, @@ -2362,6 +2362,8 @@ pub enum Precedence { DoubleColon, /// Timezone operator (e.g. `AT TIME ZONE`). AtTz, + /// Unary `+` and `-`, bounding their operand. + UnaryPlusMinus, /// Multiplication / Division / Modulo operators (`*`, `/`, `%`). MulDivModOp, /// Addition / Subtraction (`+`, `-`). diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 361396b5fe..ab63992032 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -208,6 +208,7 @@ pub struct PostgreSqlDialect {} const PERIOD_PREC: u8 = 200; const DOUBLE_COLON_PREC: u8 = 140; const BRACKET_PREC: u8 = 130; +const UNARY_PLUS_MINUS_PREC: u8 = 125; const COLLATE_PREC: u8 = 120; const AT_TZ_PREC: u8 = 110; const CARET_PREC: u8 = 100; @@ -387,6 +388,7 @@ impl Dialect for PostgreSqlDialect { Precedence::UnaryNot => NOT_PREC, Precedence::And => AND_PREC, Precedence::Or => OR_PREC, + Precedence::UnaryPlusMinus => UNARY_PLUS_MINUS_PREC, } } diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index 0124f1f645..3fbb35ebb2 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -128,7 +128,7 @@ impl Dialect for SQLiteDialect { Precedence::Period => 100, Precedence::DoubleColon => 50, Precedence::AtTz => 41, - Precedence::MulDivModOp => 40, + Precedence::MulDivModOp | Precedence::UnaryPlusMinus => 40, Precedence::PlusMinus => 30, Precedence::Xor => 25, Precedence::Ampersand => 24, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1d6cc53417..b0f8f06461 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2011,7 +2011,7 @@ impl<'a> Parser<'a> { Ok(Expr::UnaryOp { op, expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::MulDivModOp))?, + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, ), }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 9fe435e7d5..c51fa86d23 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13200,6 +13200,51 @@ fn parse_alter_table_no_inherit() { } } +#[test] +fn parse_pg_prefix_operator_before_multiplication() { + for sql in ["SELECT ~ 1 * 2", "SELECT |/ 25 * 4", "SELECT # 1 * 2"] { + let select = pg().verified_only_select(sql); + let Expr::UnaryOp { expr, .. } = expr_from_projection(&select.projection[0]) else { + panic!("Expected prefix operator expression in {sql}") + }; + assert!( + matches!( + expr.as_ref(), + Expr::BinaryOp { + op: BinaryOperator::Multiply, + .. + } + ), + "{sql}" + ); + } +} + +#[test] +fn parse_unary_plus_minus_before_pg_exponentiation() { + for (sql, unary_op, operand, exponent) in [ + ("SELECT -1 ^ 0", UnaryOperator::Minus, "1", "0"), + ("SELECT +2 ^ 2", UnaryOperator::Plus, "2", "2"), + ] { + let select = pg().verified_only_select(sql); + let Expr::BinaryOp { left, op, right } = expr_from_projection(&select.projection[0]) else { + panic!("Expected exponentiation expression") + }; + assert_eq!(*op, BinaryOperator::PGExp); + assert_eq!( + right.as_ref(), + &Expr::Value(number(exponent).with_empty_span()) + ); + assert_eq!( + left.as_ref(), + &Expr::UnaryOp { + op: unary_op, + expr: Box::new(Expr::Value(number(operand).with_empty_span())), + } + ); + } +} + #[test] fn parse_create_function_cost() { let stmt = pg_and_generic() @@ -13349,6 +13394,44 @@ fn parse_create_function_rows() { } } +#[test] +fn parse_unary_minus_before_pg_at_time_zone() { + let select = pg().verified_only_select("SELECT -ts AT TIME ZONE 'UTC'"); + let Expr::AtTimeZone { timestamp, .. } = expr_from_projection(&select.projection[0]) else { + panic!("Expected AT TIME ZONE expression") + }; + assert_eq!( + timestamp.as_ref(), + &Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Identifier(Ident::new("ts"))), + } + ); +} + +#[test] +fn parse_generic_unary_minus_before_caret() { + let statements = Parser::parse_sql(&GenericDialect {}, "SELECT -1 ^ 0").unwrap(); + let Statement::Query(query) = &statements[0] else { + panic!("Expected query") + }; + let SetExpr::Select(select) = query.body.as_ref() else { + panic!("Expected select") + }; + let Expr::BinaryOp { left, op, right } = expr_from_projection(&select.projection[0]) else { + panic!("Expected bitwise expression") + }; + assert_eq!(*op, BinaryOperator::BitwiseXor); + assert_eq!( + left.as_ref(), + &Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Value(number("1").with_empty_span())), + } + ); + assert_eq!(right.as_ref(), &Expr::Value(number("0").with_empty_span())); +} + #[test] fn parse_create_function_support() { let stmt = pg_and_generic().verified_stmt(