Skip to content

Commit d79008a

Browse files
committed
Keep the operator spelling of LIKE in step with the model
- The `~~` spelling is now derived from the keyword and `NOT`, so `setNot` or `setLikeKeyWord` no longer leave a stale operator, and `PRIOR` and `BINARY` are printed with it. `!~~~` and `null` are rejected. - A sign is also separated from an operand when either is `~`: DuckDB and PostgreSQL read `-~1` as one unknown operator. - `**` is the `OP_POWER` token; the `MulChain` alternative was never reached. - `visit(Exponentiation)` is a default method, so existing `ExpressionVisitor` implementations keep compiling.
1 parent e05834c commit d79008a

6 files changed

Lines changed: 86 additions & 53 deletions

File tree

‎src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,10 @@ default void visit(Division division) {
272272

273273
<S> T visit(IntegerDivision integerDivision, S context);
274274

275-
<S> T visit(Exponentiation exponentiation, S context);
275+
default <S> T visit(Exponentiation exponentiation, S context) {
276+
visitExpression(exponentiation.getLeftExpression(), context);
277+
return visitExpression(exponentiation.getRightExpression(), context);
278+
}
276279

277280
default void visit(Exponentiation exponentiation) {
278281
this.visit(exponentiation, null);

‎src/main/java/net/sf/jsqlparser/expression/SignedExpression.java‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ public String toString() {
6060

6161
/**
6262
* Whether the sign must be separated from an operand starting with {@code firstChar}: "- -1"
63-
* must not become the line comment "--1", nor "~ ~a" the LIKE operator "~~".
63+
* must not become the line comment "--1", nor "~ -a" or "~ ~a" a single operator such as "~-"
64+
* or the LIKE operator "~~", as DuckDB and PostgreSQL read a run of signs containing {@code ~}.
6465
*/
6566
public boolean needsSpaceBefore(char firstChar) {
66-
return (sign == '-' || sign == '~') && firstChar == sign;
67+
boolean signedOperand = firstChar == '+' || firstChar == '-' || firstChar == '~';
68+
return sign == '-' && firstChar == '-' || sign == '~' && signedOperand
69+
|| firstChar == '~' && (sign == '+' || sign == '-');
6770
}
6871

6972
private boolean needsSpaceBefore(String operand) {

‎src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class LikeExpression extends BinaryExpression implements SupportsOldOracl
1818
private boolean not = false;
1919
private boolean useBinary = false;
2020
private Expression escapeExpression = null;
21-
private String likeOperator = null;
21+
private boolean useOperator = false;
2222
private KeyWord likeKeyWord = KeyWord.LIKE;
2323

2424
public boolean isNot() {
@@ -49,23 +49,42 @@ public String getStringExpression() {
4949
return likeKeyWord.toString();
5050
}
5151

52-
/** DuckDB's and PostgreSQL's operator spelling, e.g. {@code !~~} for {@code NOT LIKE}. */
52+
/**
53+
* The operator spelling of this condition when it was written as one and has one, e.g.
54+
* {@code !~~} for {@code NOT LIKE}; otherwise {@code null} and the keyword form is used.
55+
*/
5356
public String getLikeOperator() {
54-
return likeOperator;
57+
if (!useOperator) {
58+
return null;
59+
}
60+
switch (likeKeyWord) {
61+
case LIKE:
62+
return not ? "!~~" : "~~";
63+
case ILIKE:
64+
return not ? "!~~*" : "~~*";
65+
case GLOB:
66+
return not ? null : "~~~";
67+
default:
68+
return null;
69+
}
5570
}
5671

5772
/**
58-
* Sets the operator spelling and the matching keyword: {@code ~~} is {@code LIKE}, {@code ~~*}
59-
* {@code ILIKE}, {@code ~~~} {@code GLOB}, and a leading {@code !} negates.
73+
* Sets the keyword and {@code NOT} from DuckDB's and PostgreSQL's operator spelling: {@code ~~}
74+
* is {@code LIKE}, {@code ~~*} {@code ILIKE}, {@code ~~~} {@code GLOB}, and a leading {@code !}
75+
* negates the first two. The operator is printed while it matches the keyword and {@code NOT}.
6076
*/
6177
public LikeExpression setLikeOperator(String likeOperator) {
62-
boolean negated = likeOperator.startsWith("!");
63-
String operator = negated ? likeOperator.substring(1) : likeOperator;
64-
switch (operator) {
78+
if (likeOperator == null) {
79+
throw new IllegalArgumentException("The LIKE operator must not be null");
80+
}
81+
switch (likeOperator) {
6582
case "~~":
83+
case "!~~":
6684
likeKeyWord = KeyWord.LIKE;
6785
break;
6886
case "~~*":
87+
case "!~~*":
6988
likeKeyWord = KeyWord.ILIKE;
7089
break;
7190
case "~~~":
@@ -74,21 +93,20 @@ public LikeExpression setLikeOperator(String likeOperator) {
7493
default:
7594
throw new IllegalArgumentException("Unknown LIKE operator: " + likeOperator);
7695
}
77-
this.not = negated;
78-
this.likeOperator = likeOperator;
96+
this.not = likeOperator.startsWith("!");
97+
this.useOperator = true;
7998
return this;
8099
}
81100

82101
@Override
83102
public String toString() {
84-
if (likeOperator != null) {
85-
return getLeftExpression() + " " + likeOperator + " " + getRightExpression()
86-
+ (escapeExpression != null ? " ESCAPE " + escapeExpression : "");
87-
}
103+
String likeOperator = getLikeOperator();
88104
String retval = (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "")
89-
+ getLeftExpression() + " " + (not ? "NOT " : "")
90-
+ (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord) + " "
91-
+ (useBinary ? "BINARY " : "") + getRightExpression();
105+
+ getLeftExpression() + " "
106+
+ (likeOperator != null ? likeOperator
107+
: (not ? "NOT " : "")
108+
+ (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord))
109+
+ " " + (useBinary ? "BINARY " : "") + getRightExpression();
92110
if (escapeExpression != null) {
93111
retval += " ESCAPE " + escapeExpression;
94112
}

‎src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -521,16 +521,6 @@ public <S> StringBuilder visit(JdbcParameter jdbcParameter, S context) {
521521

522522
@Override
523523
public <S> StringBuilder visit(LikeExpression likeExpression, S context) {
524-
if (likeExpression.getLikeOperator() != null) {
525-
likeExpression.getLeftExpression().accept(this, context);
526-
builder.append(" ").append(likeExpression.getLikeOperator()).append(" ");
527-
likeExpression.getRightExpression().accept(this, context);
528-
if (likeExpression.getEscape() != null) {
529-
builder.append(" ESCAPE ");
530-
likeExpression.getEscape().accept(this, context);
531-
}
532-
return builder;
533-
}
534524
String keywordStr = likeExpression.getLikeKeyWord() == LikeExpression.KeyWord.SIMILAR_TO
535525
? " SIMILAR TO"
536526
: likeExpression.getLikeKeyWord().toString();
@@ -541,10 +531,14 @@ public <S> StringBuilder visit(LikeExpression likeExpression, S context) {
541531
}
542532
likeExpression.getLeftExpression().accept(this, context);
543533
builder.append(" ");
544-
if (likeExpression.isNot()) {
545-
builder.append("NOT ");
534+
if (likeExpression.getLikeOperator() != null) {
535+
builder.append(likeExpression.getLikeOperator()).append(" ");
536+
} else {
537+
if (likeExpression.isNot()) {
538+
builder.append("NOT ");
539+
}
540+
builder.append(keywordStr).append(" ");
546541
}
547-
builder.append(keywordStr).append(" ");
548542
if (likeExpression.isUseBinary()) {
549543
builder.append("BINARY ");
550544
}

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

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
688688
|| (op == OP_CONCAT && Dialect.POSTGRESQL.name()
689689
.equals(getAsString(Feature.dialect)))) prec = 4;
690690
else if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6;
691-
else if ("**".equals(t.image)) prec = 7;
691+
else if (op == OP_POWER) prec = 7;
692692
else if (op == S_HASH_OPERATOR) prec = 5;
693693
else if (op == OP_CONCAT || op == OP_PIPE
694694
|| op == OP_LSHIFT || op == OP_RSHIFT) prec = 5;
@@ -727,7 +727,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
727727
else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
728728
else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
729729
else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
730-
else if ("**".equals(t.image)) { left = new Exponentiation(left, right); }
730+
else if (op == OP_POWER) { left = new Exponentiation(left, right); }
731731
else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
732732
else if (op == OP_PIPE) { BitwiseOr r = new BitwiseOr(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
733733
else if (op == OP_LSHIFT) { BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
@@ -3134,6 +3134,7 @@ TOKEN : /* Operators */
31343134
| <OP_CONCAT: "||">
31353135
| <OP_PIPE: "|">
31363136
| <OP_CARET: "^">
3137+
| <OP_POWER: "**">
31373138
| <OP_SLASH: "/">
31383139
| <OP_LSHIFT: "<<">
31393140
| <OP_RSHIFT: ">>">
@@ -11395,36 +11396,24 @@ Expression MulChain():
1139511396
Expression right;
1139611397
}
1139711398
{
11398-
retval=PowerChain()
11399+
retval=PrimaryExpression()
1139911400
(
1140011401
LOOKAHEAD(2)
1140111402
(
11402-
"*" right=PowerChain() {{ Multiplication r = new Multiplication(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
11403+
"*" right=PrimaryExpression() {{ Multiplication r = new Multiplication(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
1140311404
|
11404-
<OP_SLASH> right=PowerChain() {{ Division r = new Division(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
11405+
<OP_SLASH> right=PrimaryExpression() {{ Division r = new Division(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
1140511406
|
11406-
"%" right=PowerChain() {{ Modulo r = new Modulo(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
11407+
"%" right=PrimaryExpression() {{ Modulo r = new Modulo(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
1140711408
|
11408-
<OP_CARET> right=PowerChain() {{ net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
11409+
<OP_CARET> right=PrimaryExpression() {{ net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
1140911410
|
11410-
<K_DIV> right=PowerChain() {{ IntegerDivision r = new IntegerDivision(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
11411+
<K_DIV> right=PrimaryExpression() {{ IntegerDivision r = new IntegerDivision(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }}
1141111412
)
1141211413
)*
1141311414
{ return retval; }
1141411415
}
1141511416

11416-
/** DuckDB's exponentiation, above the multiplicative operators */
11417-
Expression PowerChain():
11418-
{
11419-
Expression retval;
11420-
Expression right;
11421-
}
11422-
{
11423-
retval=PrimaryExpression()
11424-
( LOOKAHEAD(2) "**" right=PrimaryExpression() { retval = new Exponentiation(retval, right); } )*
11425-
{ return retval; }
11426-
}
11427-
1142811417
/** Additive/concat/bitwise — lower precedence */
1142911418
Expression AddChain():
1143011419
{

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,32 @@ void testChainedSigns() throws JSQLParserException {
913913
Assertions.assertEquals("- - -a", CCJSqlParserUtil.parseExpression("- - -a").toString());
914914
}
915915

916+
@Test
917+
void testLikeOperatorFollowsTheModel() throws JSQLParserException {
918+
net.sf.jsqlparser.expression.operators.relational.LikeExpression like =
919+
(net.sf.jsqlparser.expression.operators.relational.LikeExpression) CCJSqlParserUtil
920+
.parseCondExpression("a ~~ 'x'");
921+
like.setNot(true);
922+
Assertions.assertEquals("a !~~ 'x'", like.toString());
923+
like.setLikeKeyWord(
924+
net.sf.jsqlparser.expression.operators.relational.LikeExpression.KeyWord.SIMILAR_TO);
925+
Assertions.assertEquals("a NOT SIMILAR TO 'x'", like.toString());
926+
Assertions.assertThrows(IllegalArgumentException.class, () -> like.setLikeOperator("!~~~"));
927+
Assertions.assertThrows(IllegalArgumentException.class, () -> like.setLikeOperator(null));
928+
}
929+
930+
@Test
931+
void testMixedSignsStaySeparate() throws JSQLParserException {
932+
for (String expression : new String[] {"- ~1", "~ -1", "~ - ~a", "- ~ -a", "+ ~a"}) {
933+
String deparsed = CCJSqlParserUtil.parseExpression(expression).toString();
934+
Assertions.assertEquals(expression.replace(" ", ""),
935+
deparsed.replace(" ", ""));
936+
Assertions.assertFalse(deparsed.contains("-~") || deparsed.contains("~-")
937+
|| deparsed.contains("~~") || deparsed.contains("+~"), deparsed);
938+
}
939+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT - ~1, ~ -1, -+1", true);
940+
}
941+
916942
private static String tree(net.sf.jsqlparser.expression.Expression expression) {
917943
if (expression instanceof net.sf.jsqlparser.expression.BinaryExpression) {
918944
net.sf.jsqlparser.expression.BinaryExpression binary =

0 commit comments

Comments
 (0)