Skip to content

Commit bfafeaa

Browse files
Match MySQL operator precedence for XOR and bitwise operators (#2838)
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
1 parent 28ad7d2 commit bfafeaa

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
903903
*/
904904
/**
905905
* Pratt boolean operator precedence loop.
906-
* Handles OR (prec=2), XOR (prec=3), AND/&& (prec=4).
906+
* Handles OR (prec=2), XOR (prec=3 under MySQL, else 2), AND/&& (prec=4).
907907
* Safe to use now that no syntactic production LOOKAHEADs scan through expressions.
908908
*/
909909
protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException {
@@ -913,7 +913,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
913913
boolean ternary = "?".equals(t.image) && isTernaryAhead();
914914
int prec;
915915
if (op == K_AND || op == OP_DOUBLEAND) prec = 4;
916-
else if (op == K_XOR || op == K_OR) prec = 2;
916+
// MySQL puts XOR between AND and OR; other dialects keep one
917+
// left-associative level for OR and XOR.
918+
else if (op == K_XOR)
919+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 2;
920+
else if (op == K_OR) prec = 2;
917921
else if (ternary) prec = 2;
918922
else break;
919923
if (prec < minPrec) break;
@@ -989,16 +993,23 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
989993
else if (op == OP_CONCAT
990994
|| (op == OP_PIPE && getToken(2).kind == OP_PIPE)) prec = 5;
991995
// the bitwise operators bind looser than + and -, as in PostgreSQL, MySQL and SQLite
992-
else if (op == S_HASH_OPERATOR || op == OP_PIPE
993-
|| op == OP_LSHIFT || op == OP_RSHIFT) prec = 4;
996+
else if (op == S_HASH_OPERATOR || op == OP_LSHIFT
997+
|| op == OP_RSHIFT) prec = 4;
998+
else if (op == OP_PIPE) {
999+
// MySQL tiers bitwise: <<, >> above & above |. Other dialects
1000+
// (PostgreSQL, SQLite, ...) keep one left-associative level.
1001+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4;
1002+
}
9941003
else {
9951004
// Handle unnamed tokens by image
9961005
String img = t.image;
9971006
// DuckDB's LIMIT 10%: the % ends the row count instead of starting a modulo
9981007
if (t == limitPercentToken) break;
9991008
if ("*".equals(img) || "%".equals(img)) prec = 6;
10001009
else if ("+".equals(img) || "-".equals(img)) prec = 5;
1001-
else if ("&".equals(img)) prec = 4;
1010+
else if ("&".equals(img)) {
1011+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 4;
1012+
}
10021013
else break; // not an arithmetic operator
10031014
}
10041015

@@ -12659,7 +12670,12 @@ Expression SimpleExpression():
1265912670
[ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ]
1266012671
retval=PrimaryExpression()
1266112672
// PostgreSQL path operators bind below arithmetic and above comparisons.
12662-
{ retval = prattArithRest(retval, 4); }
12673+
// MySQL also gives | and & their own tiers below <<, >>, so the loop's
12674+
// entry floor drops to 2 there instead of the shared bitwise level 4.
12675+
{
12676+
int arithFloor = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4;
12677+
retval = prattArithRest(retval, arithFloor);
12678+
}
1266312679
{
1266412680
if (user != null) {
1266512681
VariableAssignment assignment = new VariableAssignment();

‎src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
2323
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
2424
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
25+
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
26+
import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
2527
import net.sf.jsqlparser.expression.operators.relational.Between;
2628
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
2729
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression;
@@ -309,4 +311,38 @@ public void testSpaceSeparatedConcatStillBindsLikeAddition() throws JSQLParserEx
309311
CCJSqlParserUtil.parseExpression("a & b | | c"));
310312
Assertions.assertInstanceOf(Concat.class, and.getRightExpression());
311313
}
314+
315+
@Test
316+
public void testMySqlBitwiseOperatorsAreTiered() throws JSQLParserException {
317+
// MySQL tiers the bitwise operators: << and >> above &, & above |
318+
BitwiseOr or = Assertions.assertInstanceOf(BitwiseOr.class,
319+
CCJSqlParserUtil.parseExpression("1 | 2 & 4", false,
320+
parser -> parser.withDialect(Dialect.MYSQL)));
321+
Assertions.assertInstanceOf(BitwiseAnd.class, or.getRightExpression());
322+
323+
BitwiseAnd and = Assertions.assertInstanceOf(BitwiseAnd.class,
324+
CCJSqlParserUtil.parseExpression("3 & 1 << 2", false,
325+
parser -> parser.withDialect(Dialect.MYSQL)));
326+
Assertions.assertInstanceOf(BitwiseLeftShift.class, and.getRightExpression());
327+
328+
// the whole chain still sits below + and -, as in the default dialect
329+
BitwiseAnd shifted = Assertions.assertInstanceOf(BitwiseAnd.class,
330+
CCJSqlParserUtil.parseExpression("1 & 7 - 2", false,
331+
parser -> parser.withDialect(Dialect.MYSQL)));
332+
Assertions.assertInstanceOf(Subtraction.class, shifted.getRightExpression());
333+
}
334+
335+
@Test
336+
public void testXorBindsBetweenAndAndOr() throws JSQLParserException {
337+
// MySQL: XOR sits between AND and OR
338+
OrExpression or = Assertions.assertInstanceOf(OrExpression.class,
339+
CCJSqlParserUtil.parseExpression("1 OR 1 XOR 1", false,
340+
parser -> parser.withDialect(Dialect.MYSQL)));
341+
Assertions.assertInstanceOf(XorExpression.class, or.getRightExpression());
342+
343+
XorExpression xor = Assertions.assertInstanceOf(XorExpression.class,
344+
CCJSqlParserUtil.parseExpression("1 XOR 1 AND 1", false,
345+
parser -> parser.withDialect(Dialect.MYSQL)));
346+
Assertions.assertInstanceOf(AndExpression.class, xor.getRightExpression());
347+
}
312348
}

0 commit comments

Comments
 (0)