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
16 changes: 16 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/LambdaExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class LambdaExpression extends ASTNodeAccessImpl implements Expression {
private List<String> identifiers;
private Expression expression;
private boolean parenthesized;
private boolean lambdaKeyword;

public LambdaExpression(String identifier, Expression expression) {
this.identifiers = Collections.singletonList(identifier);
Expand Down Expand Up @@ -53,6 +54,16 @@ public LambdaExpression setParenthesized(boolean parenthesized) {
return this;
}

/** Whether it was written with DuckDB's lambda keyword: {@code lambda x, y : expr}. */
public boolean isLambdaKeyword() {
return lambdaKeyword;
}

public LambdaExpression setLambdaKeyword(boolean lambdaKeyword) {
this.lambdaKeyword = lambdaKeyword;
return this;
}

public List<String> getIdentifiers() {
return identifiers;
}
Expand All @@ -76,6 +87,11 @@ public StringBuilder appendTo(StringBuilder builder) {
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
if (lambdaKeyword) {
builder.append("lambda ").append(String.join(", ", identifiers)).append(" : ");
expressionPrinter.accept(expression);
return builder;
}
if (identifiers.size() == 1 && !parenthesized) {
builder.append(identifiers.get(0));
} else {
Expand Down
52 changes: 52 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,33 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
catch (IllegalArgumentException exception) { throw new ParseException("Unknown " + type.getSimpleName() + ": " + value); }
}

/**
* DuckDB's lambda x, y : expr. LAMBDA stays a plain identifier, so a column, type or alias
* named lambda keeps parsing: only LAMBDA followed by names separated by commas and a ":" is
* a lambda.
*/
private boolean isDuckDBLambdaAhead() {
if (getToken(1).kind != S_IDENTIFIER || !isKeywordAhead("LAMBDA")) {
return false;
}
int offset = 2;
while (true) {
int kind = getToken(offset).kind;
if (kind != S_IDENTIFIER && kind != S_QUOTED_IDENTIFIER && kind != DATA_TYPE
&& (kind < MIN_NON_RESERVED_WORD || kind > MAX_NON_RESERVED_WORD)) {
return false;
}
String next = getToken(offset + 1).image;
if (":".equals(next)) {
return true;
}
if (!",".equals(next)) {
return false;
}
offset += 2;
}
}

private boolean isKeywordAhead(String keyword) {
Token token = getToken(1);
return token.kind != EOF && token.image != null && keyword.equalsIgnoreCase(token.image);
Expand Down Expand Up @@ -11430,6 +11457,9 @@ Expression PrimaryExpression() #PrimaryExpression:
| LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() })
retval=CharacterSetIntroducedString()

// DuckDB's lambda x, y : expr, before Column() which would take LAMBDA as a column name
| LOOKAHEAD({ !interrupted && isDuckDBLambdaAhead() }) retval=DuckDBLambdaExpression()

| LOOKAHEAD(2, {!interrupted}) retval=Column()
[
LOOKAHEAD( "(" "+" ")" ("," | ")") )
Expand Down Expand Up @@ -13263,6 +13293,28 @@ LambdaExpression LambdaExpression() #LambdaExpression:
}
}

/**
* DuckDB's lambda syntax, lambda x, y : expr, equivalent to (x, y) -> expr.
*/
LambdaExpression DuckDBLambdaExpression() #LambdaExpression:
{
List<String> identifiers = new ArrayList<String>();
String s;
Expression expression;
LambdaExpression lambdaExpression;
}
{
<S_IDENTIFIER> s = RelObjectName() { identifiers.add(s); }
( "," s = RelObjectName() { identifiers.add(s); } )*
":"
expression = Expression()
{
lambdaExpression = new LambdaExpression(identifiers, expression).setLambdaKeyword(true);
linkAST(lambdaExpression,jjtThis);
return lambdaExpression;
}
}

RowPatternFunction RowPatternFunction() #RowPatternFunction:
{
RowPatternFunction.EvaluationMode mode;
Expand Down
56 changes: 56 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 @@ -10,6 +10,8 @@
package net.sf.jsqlparser.statement.select;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.LambdaExpression;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.insert.Insert;
Expand All @@ -30,6 +32,8 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

public class DuckDBTest {

@Test
Expand Down Expand Up @@ -689,4 +693,56 @@ void testRecursiveCteWithoutUsingKey() throws JSQLParserException {

Assertions.assertNull(select.getWithItemsList().get(0).getUsingKeyExpressions());
}

@Test
void testLambdaKeyword() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_transform(l, lambda x : x + 1) FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT list_filter(l, lambda x : x > 0) FROM t",
true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_filter(l, lambda x : x IS NOT NULL AND x <> 'a') FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_filter([(CASE WHEN a THEN 1 END)], lambda x : x IS NOT NULL) FROM t",
true);
TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_transform(l, lambda x : list_filter(x, lambda y : y > 1)) FROM t",
true);
}

@Test
void testLambdaKeywordExpression() throws JSQLParserException {
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true);
Function function = select.getSelectItem(0).getExpression(Function.class);
LambdaExpression lambda = (LambdaExpression) function.getParameters().get(1);

Assertions.assertTrue(lambda.isLambdaKeyword());
Assertions.assertEquals(Arrays.asList("acc", "x"), lambda.getIdentifiers());
}

@Test
void testLambdaKeywordFollowedByArguments() throws JSQLParserException {
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
"SELECT f(l, lambda x : x + 1, 2) FROM t", true);
Function function = select.getSelectItem(0).getExpression(Function.class);

Assertions.assertEquals(3, function.getParameters().size());
Assertions.assertEquals("x + 1",
((LambdaExpression) function.getParameters().get(1)).getExpression().toString());
}

@Test
void testLambdaAsIdentifier() throws JSQLParserException {
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda x, y FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT t.lambda FROM lambda t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST(a AS lambda) FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT 'a' COLLATE lambda FROM t", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a lambda)", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t DROP COLUMN lambda", true);
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t RENAME lambda TO x", true);
}
}
Loading