Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
}

default <S> T visitLimit(Limit limit, S context) {
if (limit != null && !limit.isLimitNull() && !limit.isLimitAll()) {

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 115 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitNull() in Limit has been deprecated
if (limit.getOffset() != null) {
limit.getOffset().accept(this, context);
}
Expand Down Expand Up @@ -832,6 +832,16 @@
this.visit(lambdaExpression, null);
}

default <S> 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 <S> T visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
if (expression != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
public <S> T visit(JsonExpression jsonExpr, S context) {
ArrayList<Expression> subExpressions = new ArrayList<>();
subExpressions.add(jsonExpr.getExpression());
subExpressions.addAll(jsonExpr.getIdents());

Check warning on line 561 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] getIdents() in JsonExpression has been deprecated

Check warning on line 561 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] getIdents() in JsonExpression has been deprecated

Check warning on line 561 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] getIdents() in JsonExpression has been deprecated
return visitExpressions(jsonExpr, context, subExpressions);
}

Expand Down Expand Up @@ -823,7 +823,7 @@

@Override
public <S> T visit(ConnectByRootOperator connectByRootOperator, S context) {
return connectByRootOperator.getColumn().accept(this, context);

Check warning on line 826 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated

Check warning on line 826 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated

Check warning on line 826 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] getColumn() in ConnectByRootOperator has been deprecated
}

@Override
Expand Down Expand Up @@ -918,6 +918,12 @@
return lambdaExpression.getExpression().accept(this, context);
}

@Override
public <S> T visit(ListComprehension listComprehension, S context) {
return visitExpressions(listComprehension, context, listComprehension.getExpression(),
listComprehension.getList(), listComprehension.getCondition());
}

@Override
public <S> T visit(ColumnsExpression columnsExpression, S context) {
return visitExpressions(columnsExpression, context, columnsExpression.getAllExpressions());
Expand Down
98 changes: 98 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/ListComprehension.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://duckdb.org/docs/stable/sql/functions/list#list-comprehension">List
* comprehension</a>
*/
public class ListComprehension extends ASTNodeAccessImpl implements Expression {
private Expression expression;
private List<String> variables;
private Expression list;
private Expression condition;

public ListComprehension(Expression expression, List<String> 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<String> getVariables() {
return variables;
}

public ListComprehension setVariables(List<String> 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<Expression> 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, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
return expressionVisitor.visit(this, context);
}
}
10 changes: 10 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2352,6 +2352,16 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
return null;
}

@Override
public <S> 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 <S> Void visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1882,6 +1883,12 @@ public <S> StringBuilder visit(LambdaExpression lambdaExpression, S context) {
return lambdaExpression.appendTo(builder, expression -> expression.accept(this, context));
}

@Override
public <S> StringBuilder visit(ListComprehension listComprehension, S context) {
return listComprehension.appendTo(builder,
expression -> expression.accept(this, context));
}

@Override
public <S> StringBuilder visit(ColumnsExpression columnsExpression, S context) {
columnsExpression.getColumns().accept(this, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1257,6 +1258,16 @@ public <S> Void visit(LambdaExpression lambdaExpression, S context) {
return null;
}

@Override
public <S> 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 <S> Void visit(ColumnsExpression columnsExpression, S context) {
for (Expression expression : columnsExpression.getAllExpressions()) {
Expand Down
44 changes: 44 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,30 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
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;
Expand Down Expand Up @@ -11773,6 +11797,8 @@ Expression PrimaryExpression() #PrimaryExpression:

| LOOKAHEAD(3, {!interrupted}) <K_ARRAY_LITERAL> [ "<" 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()
Expand Down Expand Up @@ -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<String> variables = new ArrayList<String>();
String variable;
Expression list;
Expression condition = null;
} {
"["
expression=Expression()
<K_FOR> variable=RelObjectName() { variables.add(variable); }
[ "," variable=RelObjectName() { variables.add(variable); } ]
<K_IN> list=Expression()
[ <K_IF> condition=Expression() ]
"]"
{ return new ListComprehension(expression, variables, list, condition); }
}

ArrayConstructor ArrayConstructor(boolean arrayKeyword) : {
ExpressionList<Expression> expList = new ExpressionList<Expression>();
Expression exp;
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Loading