diff --git a/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java b/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java index e86f34cad..74072efe3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/ArrayExpression.java @@ -17,6 +17,7 @@ public class ArrayExpression extends ASTNodeAccessImpl implements Expression { private Expression indexExpression; private Expression startIndexExpression; private Expression stopIndexExpression; + private Expression stepExpression; public ArrayExpression() { @@ -72,6 +73,19 @@ public void setStopIndexExpression(Expression stopIndexExpression) { this.stopIndexExpression = stopIndexExpression; } + public Expression getStepExpression() { + return stepExpression; + } + + public void setStepExpression(Expression stepExpression) { + this.stepExpression = stepExpression; + } + + public ArrayExpression withStepExpression(Expression stepExpression) { + this.setStepExpression(stepExpression); + return this; + } + @Override public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); @@ -82,11 +96,8 @@ public String toString() { if (indexExpression != null) { return objExpression.toString() + "[" + indexExpression.toString() + "]"; } else { - return objExpression.toString() + "[" + - (startIndexExpression == null ? "" : startIndexExpression.toString()) + - ":" + - (stopIndexExpression == null ? "" : stopIndexExpression.toString()) + - "]"; + return objExpression.toString() + "[" + RangeExpression.toString(startIndexExpression, + stopIndexExpression, stepExpression) + "]"; } } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 62ab36cae..97e5db546 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -715,6 +715,9 @@ public T visit(ArrayExpression arrayExpression, S context) { if (arrayExpression.getStopIndexExpression() != null) { subExpressions.add(arrayExpression.getStopIndexExpression()); } + if (arrayExpression.getStepExpression() != null) { + subExpressions.add(arrayExpression.getStepExpression()); + } return visitExpressions(arrayExpression, context, subExpressions); } @@ -874,7 +877,7 @@ public T visit(TrimFunction trimFunction, S context) { @Override public T visit(RangeExpression rangeExpression, S context) { return visitExpressions(rangeExpression, context, rangeExpression.getStartExpression(), - rangeExpression.getEndExpression()); + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()); } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java b/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java index dd05827d6..326f5b7da 100644 --- a/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/RangeExpression.java @@ -14,6 +14,7 @@ public class RangeExpression extends ASTNodeAccessImpl implements Expression { private Expression startExpression; private Expression endExpression; + private Expression stepExpression; public RangeExpression(Expression startExpression, Expression endExpression) { this.startExpression = startExpression; @@ -38,9 +39,42 @@ public RangeExpression setEndExpression(Expression endExpression) { return this; } + public Expression getStepExpression() { + return stepExpression; + } + + public RangeExpression setStepExpression(Expression stepExpression) { + this.stepExpression = stepExpression; + return this; + } + + public RangeExpression withStepExpression(Expression stepExpression) { + return setStepExpression(stepExpression); + } + + /** + * Renders {@code [start]:[end][:step]}, a missing end before a step as DuckDB's {@code -}. + */ + public static String toString(Expression start, Expression end, Expression step) { + StringBuilder builder = new StringBuilder(); + if (start != null) { + builder.append(start); + } + builder.append(":"); + if (end != null) { + builder.append(end); + } else if (step != null) { + builder.append("-"); + } + if (step != null) { + builder.append(":").append(step); + } + return builder.toString(); + } + @Override public String toString() { - return startExpression + ":" + endExpression; + return toString(startExpression, endExpression, stepExpression); } @Override diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 728301e04..74e8e2bad 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -350,8 +350,12 @@ public Void visit(TrimFunction trimFunction, S context) { @Override public Void visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); - rangeExpression.getEndExpression().accept(this, context); + for (Expression bound : new Expression[] {rangeExpression.getStartExpression(), + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()}) { + if (bound != null) { + bound.accept(this, context); + } + } return null; } @@ -2240,6 +2244,9 @@ public Void visit(ArrayExpression array, S context) { if (array.getStopIndexExpression() != null) { array.getStopIndexExpression().accept(this, context); } + if (array.getStepExpression() != null) { + array.getStepExpression().accept(this, context); + } return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index f7f4b187e..9f2cd6244 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -843,12 +843,27 @@ public void visit(TrimFunction trimFunction) { @Override public StringBuilder visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); - builder.append(":"); - rangeExpression.getEndExpression().accept(this, context); + appendSlice(rangeExpression.getStartExpression(), rangeExpression.getEndExpression(), + rangeExpression.getStepExpression(), context); return builder; } + private void appendSlice(Expression start, Expression end, Expression step, S context) { + if (start != null) { + start.accept(this, context); + } + builder.append(":"); + if (end != null) { + end.accept(this, context); + } else if (step != null) { + builder.append("-"); + } + if (step != null) { + builder.append(":"); + step.accept(this, context); + } + } + @Override public StringBuilder visit(TernaryExpression ternaryExpression, S context) { ternaryExpression.getCondition().accept(this, context); @@ -1645,13 +1660,8 @@ public StringBuilder visit(ArrayExpression array, S context) { if (array.getIndexExpression() != null) { array.getIndexExpression().accept(this, context); } else { - if (array.getStartIndexExpression() != null) { - array.getStartIndexExpression().accept(this, context); - } - builder.append(":"); - if (array.getStopIndexExpression() != null) { - array.getStopIndexExpression().accept(this, context); - } + appendSlice(array.getStartIndexExpression(), array.getStopIndexExpression(), + array.getStepExpression(), context); } builder.append("]"); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 2e20fd69a..83aa57eee 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -1034,6 +1034,9 @@ public Void visit(ArrayExpression array, S context) { if (array.getStopIndexExpression() != null) { array.getStopIndexExpression().accept(this, context); } + if (array.getStepExpression() != null) { + array.getStepExpression().accept(this, context); + } return null; } @@ -1200,8 +1203,12 @@ public Void visit(TrimFunction trimFunction, S context) { @Override public Void visit(RangeExpression rangeExpression, S context) { - rangeExpression.getStartExpression().accept(this, context); - rangeExpression.getEndExpression().accept(this, context); + for (Expression bound : new Expression[] {rangeExpression.getStartExpression(), + rangeExpression.getEndExpression(), rangeExpression.getStepExpression()}) { + if (bound != null) { + bound.accept(this, context); + } + } return null; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 64172751a..67f1c9501 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -203,6 +203,32 @@ public class CCJSqlParser extends AbstractJSqlParser { // and must not be taken as the JSON path operator private int mapKeyDepth = 0; + // opening brackets of the enclosing subscripts: a ":" at the top level of a subscript + // separates the bounds of a slice such as l[1:3] and is not the JSON path operator + private final Deque subscriptOpenings = new ArrayDeque(); + + /** + * Whether the pending ":" is at the top level of the innermost subscript, i.e. not nested + * in brackets, braces, parentheses or a CASE expression opened inside that subscript. + */ + private boolean isSliceColonAhead() { + Token colon = getToken(1); + if (subscriptOpenings.isEmpty() || !":".equals(colon.image)) { + return false; + } + int depth = 0; + for (Token t = subscriptOpenings.peek().next; t != null && t != colon; t = t.next) { + if (t.kind == K_CASE || "(".equals(t.image) || "[".equals(t.image) + || "{".equals(t.image)) { + depth++; + } else if (t.kind == K_END || ")".equals(t.image) || "]".equals(t.image) + || "}".equals(t.image)) { + depth--; + } + } + return depth == 0; + } + private Expression createOutputArgument(Expression argument) throws ParseException { Expression value = argument; if (argument instanceof VariableAssignment) { @@ -1571,6 +1597,10 @@ public class CCJSqlParser extends AbstractJSqlParser { || next.kind >= MIN_NON_RESERVED_WORD && next.kind <= MAX_NON_RESERVED_WORD; } + private static boolean isSliceDelimiter(Token token) { + return ":".equals(token.image) || "]".equals(token.image) || ",".equals(token.image); + } + private boolean isAliasAhead() { if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) { return false; @@ -6884,7 +6914,7 @@ Column Column() #Column : // @todo: we better should return a SEQUENCE instead of a COLUMN [ LOOKAHEAD(2) "." { data.getNames().add("nextval"); } ] - [ LOOKAHEAD(2) arrayConstructor = ArrayConstructor(false) ] + [ LOOKAHEAD(2) arrayConstructor = Subscript() ] { Column col = createColumn(data.getNames(), data.getDelimiters()); if (tk != null) { col.withCommentText(tk.image); } @@ -11341,29 +11371,85 @@ Expression AddChain(): Expression ArrayExpression(Expression obj): { Expression expr; +} { + expr = ArraySubscript(obj) + ( LOOKAHEAD(2) expr = ArraySubscript(expr) )* + { return expr; } +} + +ArrayExpression ArraySubscript(Expression obj): { Expression idxExpr = null; - Expression startExpr = null; - Expression stopExpr = null; + RangeExpression slice = null; + Token opening; } { - "[" - [LOOKAHEAD(3) idxExpr = ArrayIndexExpression()] + opening="[" + try { + { subscriptOpenings.push(opening); } + [ LOOKAHEAD(3, { !":".equals(getToken(1).image) }) idxExpr = ArrayIndexExpression() ] + [ slice = SliceTail(idxExpr) ] + } finally { + subscriptOpenings.pop(); + } + "]" + { + return slice == null + ? new ArrayExpression(obj, idxExpr, null, null) + : new ArrayExpression(obj, null, slice.getStartExpression(), + slice.getEndExpression()).withStepExpression(slice.getStepExpression()); + } +} + +/** + * The rest of a slice after its optional start: {@code :[stop][:[step]]}. As in DuckDB, a + * {@code -} stop before a step stands for the end of the list, as in {@code l[1:-:2]}. + */ +RangeExpression SliceTail(Expression start): { + Expression stop = null; + Expression step = null; +} { + ":" + [ + LOOKAHEAD("-" ":") "-" + | + LOOKAHEAD({ !isSliceDelimiter(getToken(1)) }) stop = Expression() + ] + [ + ":" [ LOOKAHEAD({ !isSliceDelimiter(getToken(1)) }) step = Expression() ] + ] + { return new RangeExpression(start, stop).withStepExpression(step); } +} + +/** A subscript of a column, such as {@code col[1]}, {@code col[1, 2]} or {@code col[1:3]}. */ +ArrayConstructor Subscript() : { + ExpressionList expList = new ExpressionList(); + Expression exp; + Token opening; +} { + opening="[" + try { + { subscriptOpenings.push(opening); } [ - (":" { startExpr=idxExpr; idxExpr=null; }) - [stopExpr = SimpleExpression()] + exp=SubscriptElement() { expList.add(exp); } + ( "," exp=SubscriptElement() { expList.add(exp); } )* ] + } finally { + subscriptOpenings.pop(); + } "]" - { expr = new ArrayExpression(obj, idxExpr, startExpr, stopExpr); } + { return new ArrayConstructor(expList, false); } +} + +Expression SubscriptElement(): +{ + Expression expression = null; +} +{ ( - LOOKAHEAD(2) "[" - [LOOKAHEAD(3) idxExpr = ArrayIndexExpression()] - [ - (":" { startExpr=idxExpr; idxExpr=null; }) - [stopExpr = SimpleExpression()] - ] - "]" - { expr = new ArrayExpression(expr, idxExpr, startExpr, stopExpr); } - )* - { return expr; } + LOOKAHEAD({ ":".equals(getToken(1).image) }) expression=SliceTail(null) + | + expression=Expression() [ expression=SliceTail(expression) ] + ) + { return expression; } } /** @@ -11618,7 +11704,7 @@ Expression PrimaryExpression() #PrimaryExpression: // Check for JSON operands [ LOOKAHEAD(2, { !isPostgreSqlJsonAccess(getToken(1)) - && ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0) + && ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0 && !isSliceColonAhead()) || !":".equals(getToken(1).image)) }) ( LOOKAHEAD(2) ( token="->" @@ -11983,7 +12069,7 @@ JsonExpression JsonExpression(Expression expr, List TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT a[0].* FROM t", true)); } + @Test + void testColumnSlices() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT l[2:3] FROM t", "SELECT l[:2] FROM t", + "SELECT l[2:] FROM t", "SELECT l[:] FROM t", "SELECT l[-5:] FROM t", + "SELECT l[1:-1:2] FROM t", "SELECT l[1:-:2] FROM t", "SELECT l[:-:-1] FROM t", + "SELECT l[x:y] FROM t", "SELECT s[2:] > 'a' FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT l[1:-:2] FROM t", true); + RangeExpression slice = (RangeExpression) select.getSelectItem(0) + .getExpression(Column.class).getArrayConstructor().getExpressions().get(0); + assertEquals("1", slice.getStartExpression().toString()); + assertNull(slice.getEndExpression()); + assertEquals("2", slice.getStepExpression().toString()); + } + + @Test + void testExpressionSlices() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT f()[2:] FROM t", "SELECT (l)[:2] FROM t", + "SELECT ([1, 2, 3])[1:-:-1]", "SELECT l[1][2:3] FROM t", + "SELECT l[:2][1] FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + ArrayExpression slice = (ArrayExpression) CCJSqlParserUtil.parseExpression("f()[x:y:2]"); + assertNull(slice.getIndexExpression()); + assertEquals("x", slice.getStartIndexExpression().toString()); + assertEquals("y", slice.getStopIndexExpression().toString()); + assertEquals("2", slice.getStepExpression().toString()); + } + + @Test + void testEmptyStepIsDropped() throws JSQLParserException { + assertEquals("l[1:2]", CCJSqlParserUtil.parseExpression("l[1:2:]").toString()); + } + + @Test + void testJsonPathOutsideSubscripts() throws JSQLParserException { + assertInstanceOf(JsonExpression.class, CCJSqlParserUtil.parseExpression("c:a[1]")); + } + + @Test + void testJsonPathNestedInSubscript() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT l[(SELECT c : a FROM t2)] FROM t", + "SELECT l[f(x, c : a)] FROM t", "SELECT l[CASE WHEN c : a = 1 THEN 1 END] FROM t", + "SELECT f()[(SELECT c : a FROM t)]"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + PlainSelect select = (PlainSelect) TestUtils + .assertSqlCanBeParsedAndDeparsed("SELECT l[ARRAY[c : a]] FROM t", true); + ArrayConstructor array = (ArrayConstructor) select.getSelectItem(0) + .getExpression(Column.class).getArrayConstructor().getExpressions().get(0); + assertInstanceOf(JsonExpression.class, array.getExpressions().get(0)); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java index b96ede8d6..6a78a0d6b 100644 --- a/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java +++ b/src/test/java/net/sf/jsqlparser/util/TablesNamesFinderTraversalTest.java @@ -18,7 +18,6 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.ArrayExpression; import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; @@ -193,11 +192,13 @@ void visitsParsedSliceBounds() throws JSQLParserException { } @Test - void preservesJsonArrayIndexTraversal() throws JSQLParserException { + void traversesSliceBounds() throws JSQLParserException { Expression expression = CCJSqlParserUtil.parseExpression("f()[1:2]"); assertThat(expression).isInstanceOf(ArrayExpression.class); - assertThat(((ArrayExpression) expression).getIndexExpression()) - .isInstanceOf(JsonExpression.class); + ArrayExpression slice = (ArrayExpression) expression; + assertThat(slice.getIndexExpression()).isNull(); + assertThat(slice.getStartIndexExpression()).hasToString("1"); + assertThat(slice.getStopIndexExpression()).hasToString("2"); assertThat(new TablesNamesFinder<>().getTables(expression)).isEmpty(); }