From 980b85a93c24db3cb05a9e3fd70a5a92a7ddff75 Mon Sep 17 00:00:00 2001 From: Suraj Suthar Date: Thu, 5 Nov 2026 20:26:54 +0530 Subject: [PATCH 1/5] PostgreSQL: Fix unary +/- precedence before exponentiation --- .../postgres_27094_unary_minus_caret.sql | 1 + src/dialect/mod.rs | 5 ++ src/dialect/postgresql.rs | 4 ++ src/parser/mod.rs | 4 +- tests/sqlparser_postgres.rs | 48 +++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql diff --git a/fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql b/fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql new file mode 100644 index 0000000000..1d1b4ffa2f --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql @@ -0,0 +1 @@ +SELECT -1 ^ 0; diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 3b3c351b9e..a292bb8c8a 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1185,6 +1185,11 @@ pub trait Dialect: Debug + Any { self.prec_value(Precedence::Between) } + /// Returns the precedence used to parse the operand of unary `+` and `-`. + fn unary_plus_minus_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..dcaa16b432 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -390,6 +390,10 @@ impl Dialect for PostgreSqlDialect { } } + fn unary_plus_minus_precedence(&self) -> u8 { + self.prec_value(Precedence::Caret) + } + fn allow_extract_custom(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 795f10a4bf..1ebb753cd2 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_plus_minus_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 841057f1da..144654b549 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13154,3 +13154,51 @@ fn parse_alter_table_no_inherit() { ); } } + +#[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_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())); +} From 6c3ab9c03f86f6999923891399fc5d74ce731127 Mon Sep 17 00:00:00 2001 From: Suraj Suthar Date: Fri, 6 Nov 2026 10:15:25 +0530 Subject: [PATCH 2/5] change to native unary plus/minus precendce --- src/dialect/mod.rs | 9 +++------ src/dialect/postgresql.rs | 6 ++---- src/dialect/sqlite.rs | 2 +- src/parser/mod.rs | 12 ++++++++---- tests/sqlparser_postgres.rs | 15 +++++++++++++++ 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index a292bb8c8a..d450c0d727 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, @@ -1185,11 +1185,6 @@ pub trait Dialect: Debug + Any { self.prec_value(Precedence::Between) } - /// Returns the precedence used to parse the operand of unary `+` and `-`. - fn unary_plus_minus_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. /// @@ -2360,6 +2355,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 dcaa16b432..d9b7aeabb5 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,13 +388,10 @@ impl Dialect for PostgreSqlDialect { Precedence::UnaryNot => NOT_PREC, Precedence::And => AND_PREC, Precedence::Or => OR_PREC, + Precedence::UnaryPlusMinus => UNARY_PLUS_MINUS_PREC, } } - fn unary_plus_minus_precedence(&self) -> u8 { - self.prec_value(Precedence::Caret) - } - fn allow_extract_custom(&self) -> bool { true } 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 1ebb753cd2..50d82fd016 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2017,7 +2017,9 @@ impl<'a> Parser<'a> { }; Ok(Expr::UnaryOp { op, - expr: Box::new(self.parse_subexpr(self.dialect.unary_plus_minus_precedence())?), + expr: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, + ), }) } Token::ExclamationMark if dialect.supports_bang_not_operator() => Ok(Expr::UnaryOp { @@ -2044,13 +2046,15 @@ impl<'a> Parser<'a> { Ok(Expr::UnaryOp { op, expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, ), }) } Token::Tilde => Ok(Expr::UnaryOp { op: UnaryOperator::BitwiseNot, - expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?), + expr: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, + ), }), tok @ Token::Sharp | tok @ Token::AtDashAt @@ -2074,7 +2078,7 @@ impl<'a> Parser<'a> { Ok(Expr::UnaryOp { op, expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, + self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, ), }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 144654b549..c50a4b1a2a 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13180,6 +13180,21 @@ fn parse_unary_plus_minus_before_pg_exponentiation() { } } +#[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(); From 878c355d83ba57098e0239b9f6a1c3720ff96e00 Mon Sep 17 00:00:00 2001 From: Suraj Suthar Date: Sat, 7 Nov 2026 14:24:27 +0530 Subject: [PATCH 3/5] removed unary prece from sqilte behaviour --- src/parser/mod.rs | 4 ++-- tests/sqlparser_postgres.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b8382be220..30dbc81e09 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2046,7 +2046,7 @@ impl<'a> Parser<'a> { Ok(Expr::UnaryOp { op, expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, + self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, ), }) } @@ -2078,7 +2078,7 @@ impl<'a> Parser<'a> { Ok(Expr::UnaryOp { op, expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, + self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?, ), }) } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 61f3592557..735d8cccbf 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -13200,6 +13200,26 @@ 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 [ @@ -13221,6 +13241,11 @@ fn parse_unary_plus_minus_before_pg_exponentiation() { op: unary_op, expr: Box::new(Expr::Value(number(operand).with_empty_span())), } + ); + } +} + +#[test] fn parse_create_function_cost() { let stmt = pg_and_generic() .verified_stmt("CREATE FUNCTION f() RETURNS INT LANGUAGE sql STABLE COST 2.5 RETURN 1"); @@ -13327,6 +13352,9 @@ fn parse_generic_unary_minus_before_caret() { } ); 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( "CREATE FUNCTION f(INT) RETURNS INT LANGUAGE c IMMUTABLE STRICT SUPPORT public.f_support AS 'obj', 'f'", From e1774c63bfd2c575655c0b3698b955661017acc6 Mon Sep 17 00:00:00 2001 From: Suraj Suthar Date: Sun, 8 Nov 2026 10:33:42 +0530 Subject: [PATCH 4/5] added plusminus precedence on tild --- src/parser/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 95c470503c..b0f8f06461 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2045,9 +2045,7 @@ impl<'a> Parser<'a> { } Token::Tilde => Ok(Expr::UnaryOp { op: UnaryOperator::BitwiseNot, - expr: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::UnaryPlusMinus))?, - ), + expr: Box::new(self.parse_subexpr(self.dialect.prec_value(Precedence::PlusMinus))?), }), tok @ Token::Sharp | tok @ Token::AtDashAt From 372ab8c15f89681d5b9c6ac64e7e1f3b125099b7 Mon Sep 17 00:00:00 2001 From: Suraj Suthar Date: Wed, 11 Nov 2026 11:39:25 +0530 Subject: [PATCH 5/5] change fuzz test name --- ..._unary_minus_caret.sql => postgres_2829_unary_minus_caret.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename fuzz/fuzz_seeds/{postgres_27094_unary_minus_caret.sql => postgres_2829_unary_minus_caret.sql} (100%) diff --git a/fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql b/fuzz/fuzz_seeds/postgres_2829_unary_minus_caret.sql similarity index 100% rename from fuzz/fuzz_seeds/postgres_27094_unary_minus_caret.sql rename to fuzz/fuzz_seeds/postgres_2829_unary_minus_caret.sql