Skip to content

Commit fde4f03

Browse files
authored
DuckDB: support the ==, ** and ~~ operators and chained signs (#2795)
* DuckDB: support the ==, ** and ~~ operators and chained signs - `a == b` is `EqualsTo` with the `==` spelling. - `a ** b` is the new `Exponentiation`, above the multiplicative operators and left-associative, as in DuckDB. - `~~`, `~~*`, `~~~`, `!~~` and `!~~*` are `LIKE`, `ILIKE`, `GLOB` and their negations, kept as written. `a ~~ 'x'` used to parse as the regular expression match `a ~ (~'x')`. - `- -1`, `+-+1` and `~ ~a` nest signs. A sign is separated from an operand starting with the same sign so that `- -1` does not deparse as the comment `--1`, nor `~ ~a` as `~~a`. * 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 49b5ea9 commit fde4f03

11 files changed

Lines changed: 288 additions & 14 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
2020
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
2121
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
22+
import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation;
2223
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
2324
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
2425
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
@@ -271,6 +272,15 @@ default void visit(Division division) {
271272

272273
<S> T visit(IntegerDivision integerDivision, S context);
273274

275+
default <S> T visit(Exponentiation exponentiation, S context) {
276+
visitExpression(exponentiation.getLeftExpression(), context);
277+
return visitExpression(exponentiation.getRightExpression(), context);
278+
}
279+
280+
default void visit(Exponentiation exponentiation) {
281+
this.visit(exponentiation, null);
282+
}
283+
274284
default void visit(IntegerDivision integerDivision) {
275285
this.visit(integerDivision, null);
276286
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
2323
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
2424
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
25+
import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation;
2526
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
2627
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
2728
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
@@ -198,6 +199,11 @@ public <S> T visit(IntegerDivision integerDivision, S context) {
198199
return visitBinaryExpression(integerDivision, context);
199200
}
200201

202+
@Override
203+
public <S> T visit(Exponentiation exponentiation, S context) {
204+
return visitBinaryExpression(exponentiation, context);
205+
}
206+
201207
@Override
202208
public <S> T visit(Multiplication multiplication, S context) {
203209
return visitBinaryExpression(multiplication, context);

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,23 @@ public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
5454

5555
@Override
5656
public String toString() {
57-
return getSign() + expression.toString();
57+
String operand = expression.toString();
58+
return getSign() + (needsSpaceBefore(operand) ? " " : "") + operand;
59+
}
60+
61+
/**
62+
* Whether the sign must be separated from an operand starting with {@code firstChar}: "- -1"
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 ~}.
65+
*/
66+
public boolean needsSpaceBefore(char firstChar) {
67+
boolean signedOperand = firstChar == '+' || firstChar == '-' || firstChar == '~';
68+
return sign == '-' && firstChar == '-' || sign == '~' && signedOperand
69+
|| firstChar == '~' && (sign == '+' || sign == '-');
70+
}
71+
72+
private boolean needsSpaceBefore(String operand) {
73+
return !operand.isEmpty() && needsSpaceBefore(operand.charAt(0));
5874
}
5975

6076
public SignedExpression withSign(char sign) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression.operators.arithmetic;
11+
12+
import net.sf.jsqlparser.expression.BinaryExpression;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.ExpressionVisitor;
15+
16+
/** DuckDB's {@code a ** b}, {@code a} raised to the power {@code b}. */
17+
public class Exponentiation extends BinaryExpression {
18+
19+
public Exponentiation() {}
20+
21+
public Exponentiation(Expression leftExpression, Expression rightExpression) {
22+
super(leftExpression, rightExpression);
23+
}
24+
25+
@Override
26+
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
27+
return expressionVisitor.visit(this, context);
28+
}
29+
30+
@Override
31+
public String getStringExpression() {
32+
return "**";
33+
}
34+
35+
@Override
36+
public Exponentiation withLeftExpression(Expression arg0) {
37+
return (Exponentiation) super.withLeftExpression(arg0);
38+
}
39+
40+
@Override
41+
public Exponentiation withRightExpression(Expression arg0) {
42+
return (Exponentiation) super.withRightExpression(arg0);
43+
}
44+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public EqualsTo() {
1818
super("=");
1919
}
2020

21+
/** {@code operator} is {@code =} or DuckDB's {@code ==}. */
22+
public EqualsTo(String operator) {
23+
super(operator);
24+
}
25+
2126
public EqualsTo(Expression left, Expression right) {
2227
this();
2328
setLeftExpression(left);

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +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 boolean useOperator = false;
2122
private KeyWord likeKeyWord = KeyWord.LIKE;
2223

2324
public boolean isNot() {
@@ -48,12 +49,64 @@ public String getStringExpression() {
4849
return likeKeyWord.toString();
4950
}
5051

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+
*/
56+
public String getLikeOperator() {
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+
}
70+
}
71+
72+
/**
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}.
76+
*/
77+
public LikeExpression setLikeOperator(String likeOperator) {
78+
if (likeOperator == null) {
79+
throw new IllegalArgumentException("The LIKE operator must not be null");
80+
}
81+
switch (likeOperator) {
82+
case "~~":
83+
case "!~~":
84+
likeKeyWord = KeyWord.LIKE;
85+
break;
86+
case "~~*":
87+
case "!~~*":
88+
likeKeyWord = KeyWord.ILIKE;
89+
break;
90+
case "~~~":
91+
likeKeyWord = KeyWord.GLOB;
92+
break;
93+
default:
94+
throw new IllegalArgumentException("Unknown LIKE operator: " + likeOperator);
95+
}
96+
this.not = likeOperator.startsWith("!");
97+
this.useOperator = true;
98+
return this;
99+
}
100+
51101
@Override
52102
public String toString() {
103+
String likeOperator = getLikeOperator();
53104
String retval = (oraclePriorPosition == ORACLE_PRIOR_START ? "PRIOR " : "")
54-
+ getLeftExpression() + " " + (not ? "NOT " : "")
55-
+ (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord) + " "
56-
+ (useBinary ? "BINARY " : "") + getRightExpression();
105+
+ getLeftExpression() + " "
106+
+ (likeOperator != null ? likeOperator
107+
: (not ? "NOT " : "")
108+
+ (likeKeyWord == KeyWord.SIMILAR_TO ? "SIMILAR TO" : likeKeyWord))
109+
+ " " + (useBinary ? "BINARY " : "") + getRightExpression();
57110
if (escapeExpression != null) {
58111
retval += " ESCAPE " + escapeExpression;
59112
}

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
2828
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
2929
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
30+
import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation;
3031
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
3132
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
3233
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
@@ -663,6 +664,12 @@ public <S> Void visit(IntegerDivision division, S context) {
663664
return null;
664665
}
665666

667+
@Override
668+
public <S> Void visit(Exponentiation exponentiation, S context) {
669+
visitBinaryExpression(exponentiation);
670+
return null;
671+
}
672+
666673
@Override
667674
public <S> Void visit(DoubleValue doubleValue, S context) {
668675

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
9494
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
9595
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
96+
import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation;
9697
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
9798
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
9899
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
@@ -225,7 +226,7 @@ public <S> StringBuilder visit(OverlapsCondition overlapsCondition, S context) {
225226

226227
@Override
227228
public <S> StringBuilder visit(EqualsTo equalsTo, S context) {
228-
deparse(equalsTo, " = ", context);
229+
deparse(equalsTo, " " + equalsTo.getStringExpression() + " ", context);
229230
return builder;
230231
}
231232

@@ -241,6 +242,12 @@ public <S> StringBuilder visit(IntegerDivision division, S context) {
241242
return builder;
242243
}
243244

245+
@Override
246+
public <S> StringBuilder visit(Exponentiation exponentiation, S context) {
247+
deparse(exponentiation, " ** ", context);
248+
return builder;
249+
}
250+
244251
@Override
245252
public <S> StringBuilder visit(DoubleValue doubleValue, S context) {
246253
builder.append(doubleValue.toString());
@@ -425,7 +432,12 @@ public <S> StringBuilder visit(FullTextSearch fullTextSearch, S context) {
425432
@Override
426433
public <S> StringBuilder visit(SignedExpression signedExpression, S context) {
427434
builder.append(signedExpression.getSign());
435+
int operandStart = builder.length();
428436
signedExpression.getExpression().accept(this, context);
437+
if (builder.length() > operandStart
438+
&& signedExpression.needsSpaceBefore(builder.charAt(operandStart))) {
439+
builder.insert(operandStart, ' ');
440+
}
429441
return builder;
430442
}
431443

@@ -519,10 +531,14 @@ public <S> StringBuilder visit(LikeExpression likeExpression, S context) {
519531
}
520532
likeExpression.getLeftExpression().accept(this, context);
521533
builder.append(" ");
522-
if (likeExpression.isNot()) {
523-
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(" ");
524541
}
525-
builder.append(keywordStr).append(" ");
526542
if (likeExpression.isUseBinary()) {
527543
builder.append("BINARY ");
528544
}

‎src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor;
9191
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
9292
import net.sf.jsqlparser.expression.operators.arithmetic.Division;
93+
import net.sf.jsqlparser.expression.operators.arithmetic.Exponentiation;
9394
import net.sf.jsqlparser.expression.operators.arithmetic.IntegerDivision;
9495
import net.sf.jsqlparser.expression.operators.arithmetic.Modulo;
9596
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
@@ -199,6 +200,12 @@ public <S> Void visit(IntegerDivision division, S context) {
199200
return null;
200201
}
201202

203+
@Override
204+
public <S> Void visit(Exponentiation exponentiation, S context) {
205+
visitBinaryExpression(exponentiation, " ** ");
206+
return null;
207+
}
208+
202209
@Override
203210
public <S> Void visit(DoubleValue doubleValue, S context) {
204211
// nothing to validate

0 commit comments

Comments
 (0)