Skip to content

Commit e86fa61

Browse files
author
Jules Ivanic
committed
Support DuckDB's lambda x, y : expr syntax
DuckDB's recommended lambda syntax, `lambda x, y : expr`, had no rule: `lambda x : x + 1` only parsed by accident, as a column named `lambda` followed by a JSON path, and a condition body (`lambda x : x > 0`, `lambda x : x IS NOT NULL`) or several parameters (`lambda acc, x : acc + x`) were rejected. These are the common forms in `list_filter` and `list_reduce`. `DuckDBLambdaExpression()` parses it into a `LambdaExpression` flagged with `isLambdaKeyword()`, which deparses it as written. `LAMBDA` is a non-reserved keyword, so it remains usable as a name.
1 parent 066f581 commit e86fa61

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class LambdaExpression extends ASTNodeAccessImpl implements Expression {
2222
private List<String> identifiers;
2323
private Expression expression;
2424
private boolean parenthesized;
25+
private boolean lambdaKeyword;
2526

2627
public LambdaExpression(String identifier, Expression expression) {
2728
this.identifiers = Collections.singletonList(identifier);
@@ -53,6 +54,16 @@ public LambdaExpression setParenthesized(boolean parenthesized) {
5354
return this;
5455
}
5556

57+
/** Whether it was written with DuckDB's lambda keyword: {@code lambda x, y : expr}. */
58+
public boolean isLambdaKeyword() {
59+
return lambdaKeyword;
60+
}
61+
62+
public LambdaExpression setLambdaKeyword(boolean lambdaKeyword) {
63+
this.lambdaKeyword = lambdaKeyword;
64+
return this;
65+
}
66+
5667
public List<String> getIdentifiers() {
5768
return identifiers;
5869
}
@@ -76,6 +87,11 @@ public StringBuilder appendTo(StringBuilder builder) {
7687
}
7788

7889
public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
90+
if (lambdaKeyword) {
91+
builder.append("lambda ").append(String.join(", ", identifiers)).append(" : ");
92+
expressionPrinter.accept(expression);
93+
return builder;
94+
}
7995
if (identifiers.size() == 1 && !parenthesized) {
8096
builder.append(identifiers.get(0));
8197
} else {

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,6 +2601,7 @@ String NonReservedWord() :
26012601
| tk=<K_KEYS:"KEYS">
26022602
| tk=<K_KILL:"KILL">
26032603
| tk=<K_FN:"FN">
2604+
| tk=<K_LAMBDA: "LAMBDA">
26042605
| tk=<K_LAST: "LAST">
26052606
| tk=<K_LEADING:"LEADING">
26062607
| tk=<K_LESS:"LESS">
@@ -11430,6 +11431,10 @@ Expression PrimaryExpression() #PrimaryExpression:
1143011431
| LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() })
1143111432
retval=CharacterSetIntroducedString()
1143211433

11434+
// DuckDB's lambda x, y : expr, before Column() which would take LAMBDA as a column name
11435+
| LOOKAHEAD( <K_LAMBDA> RelObjectName() ( "," RelObjectName() )* ":", {!interrupted} )
11436+
retval=DuckDBLambdaExpression()
11437+
1143311438
| LOOKAHEAD(2, {!interrupted}) retval=Column()
1143411439
[
1143511440
LOOKAHEAD( "(" "+" ")" ("," | ")") )
@@ -13263,6 +13268,28 @@ LambdaExpression LambdaExpression() #LambdaExpression:
1326313268
}
1326413269
}
1326513270

13271+
/**
13272+
* DuckDB's lambda syntax, lambda x, y : expr, equivalent to (x, y) -> expr.
13273+
*/
13274+
LambdaExpression DuckDBLambdaExpression() #LambdaExpression:
13275+
{
13276+
List<String> identifiers = new ArrayList<String>();
13277+
String s;
13278+
Expression expression;
13279+
LambdaExpression lambdaExpression;
13280+
}
13281+
{
13282+
<K_LAMBDA> s = RelObjectName() { identifiers.add(s); }
13283+
( "," s = RelObjectName() { identifiers.add(s); } )*
13284+
":"
13285+
expression = Expression()
13286+
{
13287+
lambdaExpression = new LambdaExpression(identifiers, expression).setLambdaKeyword(true);
13288+
linkAST(lambdaExpression,jjtThis);
13289+
return lambdaExpression;
13290+
}
13291+
}
13292+
1326613293
RowPatternFunction RowPatternFunction() #RowPatternFunction:
1326713294
{
1326813295
RowPatternFunction.EvaluationMode mode;

‎src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
package net.sf.jsqlparser.statement.select;
1111

1212
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.expression.Function;
14+
import net.sf.jsqlparser.expression.LambdaExpression;
1315
import net.sf.jsqlparser.schema.Table;
1416
import net.sf.jsqlparser.statement.create.table.CreateTable;
1517
import net.sf.jsqlparser.statement.insert.Insert;
@@ -30,6 +32,8 @@
3032
import org.junit.jupiter.api.Assertions;
3133
import org.junit.jupiter.api.Test;
3234

35+
import java.util.Arrays;
36+
3337
public class DuckDBTest {
3438

3539
@Test
@@ -689,4 +693,40 @@ void testRecursiveCteWithoutUsingKey() throws JSQLParserException {
689693

690694
Assertions.assertNull(select.getWithItemsList().get(0).getUsingKeyExpressions());
691695
}
696+
697+
@Test
698+
void testLambdaKeyword() throws JSQLParserException {
699+
TestUtils.assertSqlCanBeParsedAndDeparsed(
700+
"SELECT list_transform(l, lambda x : x + 1) FROM t", true);
701+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT list_filter(l, lambda x : x > 0) FROM t",
702+
true);
703+
TestUtils.assertSqlCanBeParsedAndDeparsed(
704+
"SELECT list_filter(l, lambda x : x IS NOT NULL AND x <> 'a') FROM t", true);
705+
TestUtils.assertSqlCanBeParsedAndDeparsed(
706+
"SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true);
707+
TestUtils.assertSqlCanBeParsedAndDeparsed(
708+
"SELECT list_filter([(CASE WHEN a THEN 1 END)], lambda x : x IS NOT NULL) FROM t",
709+
true);
710+
TestUtils.assertSqlCanBeParsedAndDeparsed(
711+
"SELECT list_transform(l, lambda x : list_filter(x, lambda y : y > 1)) FROM t",
712+
true);
713+
}
714+
715+
@Test
716+
void testLambdaKeywordExpression() throws JSQLParserException {
717+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
718+
"SELECT list_reduce(l, lambda acc, x : acc + x) FROM t", true);
719+
Function function = select.getSelectItem(0).getExpression(Function.class);
720+
LambdaExpression lambda = (LambdaExpression) function.getParameters().get(1);
721+
722+
Assertions.assertTrue(lambda.isLambdaKeyword());
723+
Assertions.assertEquals(Arrays.asList("acc", "x"), lambda.getIdentifiers());
724+
}
725+
726+
@Test
727+
void testLambdaAsIdentifier() throws JSQLParserException {
728+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda FROM t", true);
729+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda x, y FROM t", true);
730+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT t.lambda FROM lambda t", true);
731+
}
692732
}

0 commit comments

Comments
 (0)