From 447c7c9a5505c94ceb44b8aa1df77313c2f16389 Mon Sep 17 00:00:00 2001 From: compiler-segfault <2671715549@qq.com> Date: Sat, 7 Nov 2026 16:39:06 +0800 Subject: [PATCH] PostgreSQL: bind unary plus and minus tighter than `^` --- src/dialect/mod.rs | 8 +++++++ src/dialect/postgresql.rs | 8 +++++++ src/parser/mod.rs | 4 +--- tests/sqlparser_postgres.rs | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3b3c351b9e..dbc54465b3 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1185,6 +1185,14 @@ pub trait Dialect: Debug + Any { self.prec_value(Precedence::Between) } + /// Precedence used to stop parsing the operand of the unary `+` and `-` operators. + /// + /// Defaults to `MulDivModOp`, which is right for dialects where no binary operator binds + /// tighter than `*`, `/` and `%`. + fn unary_operand_precedence(&self) -> u8 { + self.prec_value(Precedence::MulDivModOp) + } + /// Returns true if the low bound of a `BETWEEN` predicate may end with one comparison, /// as in `a BETWEEN b > 1 AND c`, while `IS`, `LIKE` and `BETWEEN` still end the bound. /// diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index df81c13fd9..0abcdd922c 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -209,6 +209,10 @@ const PERIOD_PREC: u8 = 200; const DOUBLE_COLON_PREC: u8 = 140; const BRACKET_PREC: u8 = 130; const COLLATE_PREC: u8 = 120; +// `UMINUS` sits between `COLLATE`/`AT` and `'[' ']'` in gram.y, so a unary `+`/`-` operand may +// still take `.`, `::` and `[]`, but no other operator: +// +const UNARY_OPERAND_PREC: u8 = COLLATE_PREC; const AT_TZ_PREC: u8 = 110; const CARET_PREC: u8 = 100; const MUL_DIV_MOD_OP_PREC: u8 = 90; @@ -390,6 +394,10 @@ impl Dialect for PostgreSqlDialect { } } + fn unary_operand_precedence(&self) -> u8 { + UNARY_OPERAND_PREC + } + fn allow_extract_custom(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5ad3fc9639..9cb3f75b18 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2017,9 +2017,7 @@ impl<'a> Parser<'a> { }; Ok(Expr::UnaryOp { op, - expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::MulDivModOp))?, - ), + expr: Box::new(self.parse_subexpr(self.dialect.unary_operand_precedence())?), }) } Token::ExclamationMark if dialect.supports_bang_not_operator() => Ok(Expr::UnaryOp { diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 5f8cf75149..61a396b738 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13430,3 +13430,47 @@ fn display_maintain_privilege_propagates_formatter_errors() { Err(fmt::Error) ); } + +#[test] +fn parse_unary_operand_binds_tighter_than_exponentiation() { + // `UMINUS` is declared above `^` in gram.y, so `-1 ^ 0` means `(-1) ^ 0`. + let minus_one = Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::value(number("1"))), + }; + let plus_two = Expr::UnaryOp { + op: UnaryOperator::Plus, + expr: Box::new(Expr::value(number("2"))), + }; + + assert_eq!( + pg().verified_expr("-1 ^ 0"), + Expr::BinaryOp { + left: Box::new(minus_one), + op: BinaryOperator::PGExp, + right: Box::new(Expr::value(number("0"))), + } + ); + assert_eq!( + pg().verified_expr("+2 ^ 2"), + Expr::BinaryOp { + left: Box::new(plus_two), + op: BinaryOperator::PGExp, + right: Box::new(Expr::value(number("2"))), + } + ); + + // Operators that bind looser than `^` keep the unary operator on the left-hand side. + let minus_a = Expr::UnaryOp { + op: UnaryOperator::Minus, + expr: Box::new(Expr::Identifier(Ident::new("a"))), + }; + assert_eq!( + pg().verified_expr("-a * b"), + Expr::BinaryOp { + left: Box::new(minus_a), + op: BinaryOperator::Multiply, + right: Box::new(Expr::Identifier(Ident::new("b"))), + } + ); +}