diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 6a4fca3ac..e27201624 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -832,6 +832,16 @@ default void visit(LambdaExpression lambdaExpression) { this.visit(lambdaExpression, null); } + default T visit(ListComprehension listComprehension, S context) { + visitExpression(listComprehension.getExpression(), context); + visitExpression(listComprehension.getList(), context); + return visitExpression(listComprehension.getCondition(), context); + } + + default void visit(ListComprehension listComprehension) { + this.visit(listComprehension, null); + } + default T visit(ColumnsExpression columnsExpression, S context) { for (Expression expression : columnsExpression.getAllExpressions()) { if (expression != null) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 3f75f259e..c5ed89985 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -918,6 +918,12 @@ public T visit(LambdaExpression lambdaExpression, S context) { return lambdaExpression.getExpression().accept(this, context); } + @Override + public T visit(ListComprehension listComprehension, S context) { + return visitExpressions(listComprehension, context, listComprehension.getExpression(), + listComprehension.getList(), listComprehension.getCondition()); + } + @Override public T visit(ColumnsExpression columnsExpression, S context) { return visitExpressions(columnsExpression, context, columnsExpression.getAllExpressions()); diff --git a/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java b/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java new file mode 100644 index 000000000..e65fab885 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/ListComprehension.java @@ -0,0 +1,98 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.List; +import java.util.function.Consumer; + +/** + * DuckDB's list comprehension {@code [expression FOR x[, i] IN list [IF condition]]}, where + * {@code i} is the 1-based index of {@code x}. + * + * @see List + * comprehension + */ +public class ListComprehension extends ASTNodeAccessImpl implements Expression { + private Expression expression; + private List variables; + private Expression list; + private Expression condition; + + public ListComprehension(Expression expression, List variables, Expression list, + Expression condition) { + this.expression = expression; + this.variables = variables; + this.list = list; + this.condition = condition; + } + + public Expression getExpression() { + return expression; + } + + public ListComprehension setExpression(Expression expression) { + this.expression = expression; + return this; + } + + /** The element variable, followed by the index variable if any. */ + public List getVariables() { + return variables; + } + + public ListComprehension setVariables(List variables) { + this.variables = variables; + return this; + } + + public Expression getList() { + return list; + } + + public ListComprehension setList(Expression list) { + this.list = list; + return this; + } + + /** The {@code IF} filter, or {@code null}. */ + public Expression getCondition() { + return condition; + } + + public ListComprehension setCondition(Expression condition) { + this.condition = condition; + return this; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) { + builder.append("["); + expressionRenderer.accept(expression); + builder.append(" FOR ").append(String.join(", ", variables)).append(" IN "); + expressionRenderer.accept(list); + if (condition != null) { + builder.append(" IF "); + expressionRenderer.accept(condition); + } + return builder.append("]"); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 389eefeb2..9d951a409 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -2352,6 +2352,16 @@ public Void visit(LambdaExpression lambdaExpression, S context) { return null; } + @Override + public Void visit(ListComprehension listComprehension, S context) { + listComprehension.getExpression().accept(this, context); + listComprehension.getList().accept(this, context); + if (listComprehension.getCondition() != null) { + listComprehension.getCondition().accept(this, context); + } + return null; + } + @Override public Void visit(ColumnsExpression columnsExpression, S context) { for (Expression expression : columnsExpression.getAllExpressions()) { 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 6ba93d4bf..d0700d731 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -51,6 +51,7 @@ import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.ListComprehension; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; import net.sf.jsqlparser.expression.MapExpression; @@ -1882,6 +1883,12 @@ public StringBuilder visit(LambdaExpression lambdaExpression, S context) { return lambdaExpression.appendTo(builder, expression -> expression.accept(this, context)); } + @Override + public StringBuilder visit(ListComprehension listComprehension, S context) { + return listComprehension.appendTo(builder, + expression -> expression.accept(this, context)); + } + @Override public StringBuilder visit(ColumnsExpression columnsExpression, S context) { columnsExpression.getColumns().accept(this, context); 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 4a76de399..6b2d4927c 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 @@ -48,6 +48,7 @@ import net.sf.jsqlparser.expression.KeepExpression; import net.sf.jsqlparser.expression.KeyExpression; import net.sf.jsqlparser.expression.LambdaExpression; +import net.sf.jsqlparser.expression.ListComprehension; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LowExpression; import net.sf.jsqlparser.expression.MapExpression; @@ -1257,6 +1258,16 @@ public Void visit(LambdaExpression lambdaExpression, S context) { return null; } + @Override + public Void visit(ListComprehension listComprehension, S context) { + listComprehension.getExpression().accept(this, context); + listComprehension.getList().accept(this, context); + if (listComprehension.getCondition() != null) { + listComprehension.getCondition().accept(this, context); + } + return null; + } + @Override public Void visit(ColumnsExpression columnsExpression, S context) { for (Expression expression : columnsExpression.getAllExpressions()) { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 730eace0f..7e6fe5ebc 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1689,6 +1689,30 @@ public class CCJSqlParser extends AbstractJSqlParser { return ":".equals(token.image) || "]".equals(token.image) || ",".equals(token.image); } + /** Whether a {@code [} opens DuckDB's {@code [expr FOR x IN list]}: FOR at its top level. */ + private boolean isListComprehensionAhead() { + Token token = getToken(1); + if (!"[".equals(token.image)) { + return false; + } + int depth = 0; + for (;; token = nextWithItemToken(token)) { + if (token.kind == EOF || ";".equals(token.image)) { + return false; + } + if ("(".equals(token.image) || "[".equals(token.image) || "{".equals(token.image)) { + depth++; + } else if (")".equals(token.image) || "]".equals(token.image) + || "}".equals(token.image)) { + if (--depth == 0) { + return false; + } + } else if (depth == 1 && token.kind == K_FOR) { + return true; + } + } + } + private boolean isAliasAhead() { if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) { return false; @@ -11773,6 +11797,8 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(3, {!interrupted}) [ "<" type=ColDataType() ">" ] retval=ArrayConstructor(true) { if (type!=null) ((ArrayConstructor) retval).setDataType(type); } + | LOOKAHEAD({ !interrupted && isListComprehensionAhead() }) retval=ListComprehension() + | LOOKAHEAD(1, {!interrupted}) retval=ArrayConstructor(false) | LOOKAHEAD(2, {!interrupted}) retval = NextValExpression() @@ -12081,6 +12107,24 @@ RangeExpression RangeExpression(Expression startExpression): } } +/** DuckDB's list comprehension {@code [expr FOR x[, i] IN list [IF condition]]}. */ +ListComprehension ListComprehension() : { + Expression expression; + List variables = new ArrayList(); + String variable; + Expression list; + Expression condition = null; +} { + "[" + expression=Expression() + variable=RelObjectName() { variables.add(variable); } + [ "," variable=RelObjectName() { variables.add(variable); } ] + list=Expression() + [ condition=Expression() ] + "]" + { return new ListComprehension(expression, variables, list, condition); } +} + ArrayConstructor ArrayConstructor(boolean arrayKeyword) : { ExpressionList expList = new ExpressionList(); Expression exp; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java index 726528dce..f7ea78cfe 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java @@ -34,6 +34,7 @@ import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import java.util.Arrays; import java.util.HashSet; @@ -986,4 +987,38 @@ private static String tree(net.sf.jsqlparser.expression.Expression expression) { } return expression.toString(); } + + @Test + void testListComprehension() throws JSQLParserException { + for (String sqlStr : new String[] {"SELECT [x * 2 FOR x IN l] FROM t", + "SELECT [x FOR x IN l IF x > 1] FROM t", "SELECT [x + i FOR x, i IN l] FROM t", + "SELECT [[y FOR y IN x] FOR x IN [[1], [2]]]", + "SELECT [x FOR x IN l IF x IN (1, 2)] FROM t", + "SELECT [x FOR x IN (SELECT list(a) FROM t)]", + "SELECT [x FOR x IN l][1] FROM t"}) { + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + net.sf.jsqlparser.expression.ListComprehension comprehension = + (net.sf.jsqlparser.expression.ListComprehension) CCJSqlParserUtil + .parseExpression("[x + i FOR x, i IN l IF x > 1]"); + Assertions.assertEquals("x + i", comprehension.getExpression().toString()); + Assertions.assertEquals(Arrays.asList("x", "i"), comprehension.getVariables()); + Assertions.assertEquals("l", comprehension.getList().toString()); + Assertions.assertEquals("x > 1", comprehension.getCondition().toString()); + + Assertions.assertEquals(new HashSet<>(Arrays.asList("t", "u")), + new HashSet<>(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse( + "SELECT [x FOR x IN (SELECT list(a) FROM u)] FROM t")))); + } + + @Test + @Timeout(10) + void testLargeArrayIsScannedOnce() throws JSQLParserException { + StringBuilder sql = new StringBuilder("SELECT [0"); + for (int i = 1; i < 20000; i++) { + sql.append(", ").append(i); + } + CCJSqlParserUtil.parse(sql.append("]").toString()); + } }