Skip to content

Commit eba5002

Browse files
guizmaiiJules Ivanic
andauthored
Support DuckDB's lambda x, y : expr syntax (#2782)
* 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. * Keep `lambda` a plain identifier outside DuckDB lambdas As a keyword token, LAMBDA stopped parsing where only plain identifiers are accepted: `CAST(a AS lambda)`, `COLLATE lambda`, `CREATE TABLE t (a lambda)`, `ALTER TABLE t DROP COLUMN lambda`, `ALTER TABLE t RENAME lambda TO x`. `isDuckDBLambdaAhead()` recognises `lambda name [, name ...] :` contextually, as `isKeywordAhead()` does for other words, so no token is needed. --------- Co-authored-by: Jules Ivanic <jules.ivanic@feedzai.com>
1 parent d709c4e commit eba5002

3 files changed

Lines changed: 124 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: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,33 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
15841584
catch (IllegalArgumentException exception) { throw new ParseException("Unknown " + type.getSimpleName() + ": " + value); }
15851585
}
15861586

1587+
/**
1588+
* DuckDB's lambda x, y : expr. LAMBDA stays a plain identifier, so a column, type or alias
1589+
* named lambda keeps parsing: only LAMBDA followed by names separated by commas and a ":" is
1590+
* a lambda.
1591+
*/
1592+
private boolean isDuckDBLambdaAhead() {
1593+
if (getToken(1).kind != S_IDENTIFIER || !isKeywordAhead("LAMBDA")) {
1594+
return false;
1595+
}
1596+
int offset = 2;
1597+
while (true) {
1598+
int kind = getToken(offset).kind;
1599+
if (kind != S_IDENTIFIER && kind != S_QUOTED_IDENTIFIER && kind != DATA_TYPE
1600+
&& (kind < MIN_NON_RESERVED_WORD || kind > MAX_NON_RESERVED_WORD)) {
1601+
return false;
1602+
}
1603+
String next = getToken(offset + 1).image;
1604+
if (":".equals(next)) {
1605+
return true;
1606+
}
1607+
if (!",".equals(next)) {
1608+
return false;
1609+
}
1610+
offset += 2;
1611+
}
1612+
}
1613+
15871614
private boolean isKeywordAhead(String keyword) {
15881615
Token token = getToken(1);
15891616
return token.kind != EOF && token.image != null && keyword.equalsIgnoreCase(token.image);
@@ -11415,6 +11442,9 @@ Expression PrimaryExpression() #PrimaryExpression:
1141511442
| LOOKAHEAD({ !interrupted && isCharacterSetIntroducedStringAhead() })
1141611443
retval=CharacterSetIntroducedString()
1141711444

11445+
// DuckDB's lambda x, y : expr, before Column() which would take LAMBDA as a column name
11446+
| LOOKAHEAD({ !interrupted && isDuckDBLambdaAhead() }) retval=DuckDBLambdaExpression()
11447+
1141811448
| LOOKAHEAD(2, {!interrupted}) retval=Column()
1141911449
[
1142011450
LOOKAHEAD( "(" "+" ")" ("," | ")") )
@@ -13248,6 +13278,28 @@ LambdaExpression LambdaExpression() #LambdaExpression:
1324813278
}
1324913279
}
1325013280

13281+
/**
13282+
* DuckDB's lambda syntax, lambda x, y : expr, equivalent to (x, y) -> expr.
13283+
*/
13284+
LambdaExpression DuckDBLambdaExpression() #LambdaExpression:
13285+
{
13286+
List<String> identifiers = new ArrayList<String>();
13287+
String s;
13288+
Expression expression;
13289+
LambdaExpression lambdaExpression;
13290+
}
13291+
{
13292+
<S_IDENTIFIER> s = RelObjectName() { identifiers.add(s); }
13293+
( "," s = RelObjectName() { identifiers.add(s); } )*
13294+
":"
13295+
expression = Expression()
13296+
{
13297+
lambdaExpression = new LambdaExpression(identifiers, expression).setLambdaKeyword(true);
13298+
linkAST(lambdaExpression,jjtThis);
13299+
return lambdaExpression;
13300+
}
13301+
}
13302+
1325113303
RowPatternFunction RowPatternFunction() #RowPatternFunction:
1325213304
{
1325313305
RowPatternFunction.EvaluationMode mode;

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

Lines changed: 56 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,56 @@ 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 testLambdaKeywordFollowedByArguments() throws JSQLParserException {
728+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(
729+
"SELECT f(l, lambda x : x + 1, 2) FROM t", true);
730+
Function function = select.getSelectItem(0).getExpression(Function.class);
731+
732+
Assertions.assertEquals(3, function.getParameters().size());
733+
Assertions.assertEquals("x + 1",
734+
((LambdaExpression) function.getParameters().get(1)).getExpression().toString());
735+
}
736+
737+
@Test
738+
void testLambdaAsIdentifier() throws JSQLParserException {
739+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda FROM t", true);
740+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT lambda x, y FROM t", true);
741+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT t.lambda FROM lambda t", true);
742+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT CAST(a AS lambda) FROM t", true);
743+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT 'a' COLLATE lambda FROM t", true);
744+
TestUtils.assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (a lambda)", true);
745+
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t DROP COLUMN lambda", true);
746+
TestUtils.assertSqlCanBeParsedAndDeparsed("ALTER TABLE t RENAME lambda TO x", true);
747+
}
692748
}

0 commit comments

Comments
 (0)