Skip to content

Commit 877e628

Browse files
committed
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
1 parent f6ac257 commit 877e628

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
@@ -688,7 +688,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
688688
*/
689689
/**
690690
* Pratt boolean operator precedence loop.
691-
* Handles OR (prec=2), XOR (prec=3), AND/&& (prec=4).
691+
* Handles OR (prec=2), XOR (prec=3 under MySQL, else 2), AND/&& (prec=4).
692692
* Safe to use now that no syntactic production LOOKAHEADs scan through expressions.
693693
*/
694694
protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException {
@@ -698,7 +698,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
698698
boolean ternary = "?".equals(t.image) && isTernaryAhead();
699699
int prec;
700700
if (op == K_AND || op == OP_DOUBLEAND) prec = 4;
701-
else if (op == K_XOR || op == K_OR) prec = 2;
701+
// MySQL puts XOR between AND and OR; other dialects keep one
702+
// left-associative level for OR and XOR.
703+
else if (op == K_XOR)
704+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 2;
705+
else if (op == K_OR) prec = 2;
702706
else if (ternary) prec = 2;
703707
else break;
704708
if (prec < minPrec) break;
@@ -765,16 +769,23 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
765769
else if (op == OP_CONCAT
766770
|| (op == OP_PIPE && getToken(2).kind == OP_PIPE)) prec = 5;
767771
// the bitwise operators bind looser than + and -, as in PostgreSQL, MySQL and SQLite
768-
else if (op == S_HASH_OPERATOR || op == OP_PIPE
769-
|| op == OP_LSHIFT || op == OP_RSHIFT) prec = 4;
772+
else if (op == S_HASH_OPERATOR || op == OP_LSHIFT
773+
|| op == OP_RSHIFT) prec = 4;
774+
else if (op == OP_PIPE) {
775+
// MySQL tiers bitwise: <<, >> above & above |. Other dialects
776+
// (PostgreSQL, SQLite, ...) keep one left-associative level.
777+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4;
778+
}
770779
else {
771780
// Handle unnamed tokens by image
772781
String img = t.image;
773782
// DuckDB's LIMIT 10%: the % ends the row count instead of starting a modulo
774783
if (t == limitPercentToken) break;
775784
if ("*".equals(img) || "%".equals(img)) prec = 6;
776785
else if ("+".equals(img) || "-".equals(img)) prec = 5;
777-
else if ("&".equals(img)) prec = 4;
786+
else if ("&".equals(img)) {
787+
prec = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 3 : 4;
788+
}
778789
else break; // not an arithmetic operator
779790
}
780791

@@ -11869,7 +11880,12 @@ Expression SimpleExpression():
1186911880
[ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ]
1187011881
retval=PrimaryExpression()
1187111882
// PostgreSQL path operators bind below arithmetic and above comparisons.
11872-
{ retval = prattArithRest(retval, 4); }
11883+
// MySQL also gives | and & their own tiers below <<, >>, so the loop's
11884+
// entry floor drops to 2 there instead of the shared bitwise level 4.
11885+
{
11886+
int arithFloor = Dialect.MYSQL.name().equals(getAsString(Feature.dialect)) ? 2 : 4;
11887+
retval = prattArithRest(retval, arithFloor);
11888+
}
1187311889
{
1187411890
if (user != null) {
1187511891
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)