From 877e628a1a0a1c8f43ec5fe49cfb4aaa822120b3 Mon Sep 17 00:00:00 2001 From: princejain756 Date: Wed, 21 Oct 2026 08:42:20 +0530 Subject: [PATCH] Match MySQL operator precedence for XOR and bitwise operators XOR sat on the same level as OR, and |, &, <<, >> shared one left-associative level, in every dialect. MySQL puts XOR between AND and OR, and tiers the bitwise operators as <<, >> above & above |. Gate the tiers on Dialect.MYSQL so the default dialect keeps the shared levels established in #2833. SimpleExpression now enters the arithmetic loop at floor 2 under MySQL so | is admitted. Fixes #2834, fixes #2835 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 +++++++++++---- .../expression/ExpressionPrecedenceTest.java | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f49b3ddbae..fad99c8fc8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -688,7 +688,7 @@ public class CCJSqlParser extends AbstractJSqlParser { */ /** * Pratt boolean operator precedence loop. - * Handles OR (prec=2), XOR (prec=3), AND/&& (prec=4). + * Handles OR (prec=2), XOR (prec=3 under MySQL, else 2), AND/&& (prec=4). * Safe to use now that no syntactic production LOOKAHEADs scan through expressions. */ protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException { @@ -698,7 +698,11 @@ public class CCJSqlParser extends AbstractJSqlParser { boolean ternary = "?".equals(t.image) && isTernaryAhead(); int prec; if (op == K_AND || op == OP_DOUBLEAND) prec = 4; - else if (op == K_XOR || op == K_OR) prec = 2; + // MySQL puts XOR between AND and OR; other dialects keep one + // left-associative level for OR and XOR. + else if (op == K_XOR) + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 2; + else if (op == K_OR) prec = 2; else if (ternary) prec = 2; else break; if (prec < minPrec) break; @@ -765,8 +769,13 @@ public class CCJSqlParser extends AbstractJSqlParser { else if (op == OP_CONCAT || (op == OP_PIPE && getToken(2).kind == OP_PIPE)) prec = 5; // the bitwise operators bind looser than + and -, as in PostgreSQL, MySQL and SQLite - else if (op == S_HASH_OPERATOR || op == OP_PIPE - || op == OP_LSHIFT || op == OP_RSHIFT) prec = 4; + else if (op == S_HASH_OPERATOR || op == OP_LSHIFT + || op == OP_RSHIFT) prec = 4; + else if (op == OP_PIPE) { + // MySQL tiers bitwise: <<, >> above & above |. Other dialects + // (PostgreSQL, SQLite, ...) keep one left-associative level. + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4; + } else { // Handle unnamed tokens by image String img = t.image; @@ -774,7 +783,9 @@ public class CCJSqlParser extends AbstractJSqlParser { if (t == limitPercentToken) break; if ("*".equals(img) || "%".equals(img)) prec = 6; else if ("+".equals(img) || "-".equals(img)) prec = 5; - else if ("&".equals(img)) prec = 4; + else if ("&".equals(img)) { + prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 4; + } else break; // not an arithmetic operator } @@ -11869,7 +11880,12 @@ Expression SimpleExpression(): [ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] retval=PrimaryExpression() // PostgreSQL path operators bind below arithmetic and above comparisons. - { retval = prattArithRest(retval, 4); } + // MySQL also gives | and & their own tiers below <<, >>, so the loop's + // entry floor drops to 2 there instead of the shared bitwise level 4. + { + int arithFloor = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4; + retval = prattArithRest(retval, arithFloor); + } { if (user != null) { VariableAssignment assignment = new VariableAssignment(); diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java index 980ffbe1da..040607ddde 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java @@ -22,6 +22,8 @@ import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication; import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.conditional.XorExpression; import net.sf.jsqlparser.expression.operators.relational.Between; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; @@ -309,4 +311,38 @@ public void testSpaceSeparatedConcatStillBindsLikeAddition() throws JSQLParserEx CCJSqlParserUtil.parseExpression("a & b | | c")); Assertions.assertInstanceOf(Concat.class, and.getRightExpression()); } + + @Test + public void testMySqlBitwiseOperatorsAreTiered() throws JSQLParserException { + // MySQL tiers the bitwise operators: << and >> above &, & above | + BitwiseOr or = Assertions.assertInstanceOf(BitwiseOr.class, + CCJSqlParserUtil.parseExpression("1 | 2 & 4", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(BitwiseAnd.class, or.getRightExpression()); + + BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("3 & 1 << 2", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(BitwiseLeftShift.class, and.getRightExpression()); + + // the whole chain still sits below + and -, as in the default dialect + BitwiseAnd shifted = Assertions.assertInstanceOf(BitwiseAnd.class, + CCJSqlParserUtil.parseExpression("1 & 7 - 2", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(Subtraction.class, shifted.getRightExpression()); + } + + @Test + public void testXorBindsBetweenAndAndOr() throws JSQLParserException { + // MySQL: XOR sits between AND and OR + OrExpression or = Assertions.assertInstanceOf(OrExpression.class, + CCJSqlParserUtil.parseExpression("1 OR 1 XOR 1", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(XorExpression.class, or.getRightExpression()); + + XorExpression xor = Assertions.assertInstanceOf(XorExpression.class, + CCJSqlParserUtil.parseExpression("1 XOR 1 AND 1", false, + parser -> parser.withDialect(Dialect.MYSQL))); + Assertions.assertInstanceOf(AndExpression.class, xor.getRightExpression()); + } }