This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git
The following commit(s) were added to refs/heads/main by this push:
new cd5ca0bf Separate the LAMBDA keyword syntax from the -> lambda syntax
(#2458)
cd5ca0bf is described below
commit cd5ca0bfbcd3d7c7fd5799a271af77e81a1fe490
Author: Adrian Garcia Badaracco <[email protected]>
AuthorDate: Tue Sep 22 09:30:30 2026 +0000
Separate the LAMBDA keyword syntax from the -> lambda syntax (#2458)
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: Luca Cappelletti
<[email protected]>
Co-authored-by: Luca Cappelletti <[email protected]>
---
src/dialect/mod.rs | 24 ++++++++
src/parser/mod.rs | 2 +-
tests/sqlparser_custom_dialect.rs | 117 ++++++++++++++++++++++++++++++++++++++
tests/sqlparser_derive_dialect.rs | 69 ++++++++++++++++++++++
tests/sqlparser_duckdb.rs | 15 +++++
5 files changed, 226 insertions(+), 1 deletion(-)
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 7c4744c5..1af00226 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -535,10 +535,34 @@ pub trait Dialect: Debug + Any {
/// ```sql
/// SELECT transform(array(1, 2, 3), x -> x + 1); -- returns [2,3,4]
/// ```
+ ///
+ /// This enables both the `->` spelling above and the `LAMBDA` keyword
+ /// spelling gated by [`Self::supports_lambda_keyword_syntax`]. A dialect
+ /// that uses `->` as a binary operator should override only the latter.
fn supports_lambda_functions(&self) -> bool {
false
}
+ /// Returns true if the dialect supports the `LAMBDA` keyword spelling of
+ /// lambda functions, for example:
+ ///
+ /// ```sql
+ /// SELECT list_transform([1, 2, 3], lambda x : x + 1); -- returns [2, 3,
4]
+ /// ```
+ ///
+ /// This spelling does not claim the `->` token, so it can be enabled by
+ /// dialects that already give `->` a different meaning, such as JSON
+ /// member access.
+ ///
+ /// Defaults to [`Self::supports_lambda_functions`], so dialects supporting
+ /// the `->` spelling accept the `LAMBDA` spelling too unless they say
+ /// otherwise.
+ ///
+ /// See <https://duckdb.org/docs/stable/sql/functions/lambda>
+ fn supports_lambda_keyword_syntax(&self) -> bool {
+ self.supports_lambda_functions()
+ }
+
/// Returns true if the dialect supports multiple variable assignment
/// using parentheses in a `SET` variable declaration.
///
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 15f135ff..74d97a08 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1647,7 +1647,7 @@ impl<'a> Parser<'a> {
})
})
}
- Keyword::LAMBDA if self.dialect.supports_lambda_functions() => {
+ Keyword::LAMBDA if self.dialect.supports_lambda_keyword_syntax()
=> {
Ok(Some(self.parse_lambda_expr()?))
}
_ if self.dialect.supports_geometric_types() => match w.keyword {
diff --git a/tests/sqlparser_custom_dialect.rs
b/tests/sqlparser_custom_dialect.rs
index cee604ac..5bf38dde 100644
--- a/tests/sqlparser_custom_dialect.rs
+++ b/tests/sqlparser_custom_dialect.rs
@@ -22,6 +22,7 @@ use sqlparser::{
dialect::Dialect,
keywords::Keyword,
parser::{Parser, ParserError},
+ test_utils::{expr_from_projection, only},
tokenizer::Token,
};
@@ -167,3 +168,119 @@ fn is_identifier_part(ch: char) -> bool {
|| ch == '$'
|| ch == '_'
}
+
+#[test]
+fn custom_dialect_lambda_keyword_syntax_without_arrow() {
+ // A dialect that gives `->` its own meaning can still support lambdas
+ // through the `LAMBDA` keyword spelling.
+ #[derive(Debug)]
+ struct MyDialect {}
+
+ impl Dialect for MyDialect {
+ fn is_identifier_start(&self, ch: char) -> bool {
+ is_identifier_start(ch)
+ }
+
+ fn is_identifier_part(&self, ch: char) -> bool {
+ is_identifier_part(ch)
+ }
+
+ fn supports_lambda_keyword_syntax(&self) -> bool {
+ true
+ }
+ }
+
+ let dialect = MyDialect {};
+
+ // The `LAMBDA` spelling parses.
+ let sql = "SELECT transform(xs, lambda x : x + 1)";
+ assert_eq!(
+ sql,
+ &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
+ );
+
+ // `->` keeps whatever meaning the dialect gives it, rather than
+ // introducing a lambda parameter.
+ let sql = "SELECT a -> 'b'";
+ let ast = Parser::parse_sql(&dialect, sql).unwrap();
+ match &ast[0] {
+ Statement::Query(query) => {
+ let Expr::BinaryOp { op, .. } =
+
expr_from_projection(only(&query.body.as_select().unwrap().projection))
+ else {
+ panic!("expected `->` to stay a binary operator");
+ };
+ assert_eq!(&BinaryOperator::Arrow, op);
+ }
+ stmt => panic!("unexpected statement {stmt}"),
+ }
+}
+
+#[test]
+fn custom_dialect_lambda_keyword_defaults_to_arrow_support() {
+ // Dialects that opt into the `->` spelling get the `LAMBDA` spelling too,
+ // so the new capability does not change any existing dialect.
+ #[derive(Debug)]
+ struct MyDialect {}
+
+ impl Dialect for MyDialect {
+ fn is_identifier_start(&self, ch: char) -> bool {
+ is_identifier_start(ch)
+ }
+
+ fn is_identifier_part(&self, ch: char) -> bool {
+ is_identifier_part(ch)
+ }
+
+ fn supports_lambda_functions(&self) -> bool {
+ true
+ }
+ }
+
+ let dialect = MyDialect {};
+ assert!(dialect.supports_lambda_keyword_syntax());
+ for sql in [
+ "SELECT transform(xs, lambda x : x + 1)",
+ "SELECT transform(xs, x -> x + 1)",
+ ] {
+ assert_eq!(
+ sql,
+ &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
+ );
+ }
+}
+
+#[test]
+fn custom_dialect_lambda_arrow_syntax_without_keyword() {
+ // Arrow lambdas stay on while the `LAMBDA` keyword spelling is off,
+ // as in engines like Spark and Snowflake.
+ #[derive(Debug)]
+ struct MyDialect {}
+
+ impl Dialect for MyDialect {
+ fn is_identifier_start(&self, ch: char) -> bool {
+ is_identifier_start(ch)
+ }
+
+ fn is_identifier_part(&self, ch: char) -> bool {
+ is_identifier_part(ch)
+ }
+
+ fn supports_lambda_functions(&self) -> bool {
+ true
+ }
+
+ fn supports_lambda_keyword_syntax(&self) -> bool {
+ false
+ }
+ }
+
+ let dialect = MyDialect {};
+
+ let sql = "SELECT transform(xs, x -> x + 1)";
+ assert_eq!(
+ sql,
+ &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0])
+ );
+ assert!(Parser::parse_sql(&dialect, "SELECT transform(xs, lambda x : x +
1)").is_err());
+}
diff --git a/tests/sqlparser_derive_dialect.rs
b/tests/sqlparser_derive_dialect.rs
index d60fa1e1..6320556f 100644
--- a/tests/sqlparser_derive_dialect.rs
+++ b/tests/sqlparser_derive_dialect.rs
@@ -17,9 +17,13 @@
//! Tests for the `derive_dialect!` macro.
+use sqlparser::ast::{
+ BinaryOperator, Expr, FunctionArg, FunctionArgExpr, FunctionArguments,
LambdaSyntax, Statement,
+};
use sqlparser::derive_dialect;
use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect,
PostgreSqlDialect};
use sqlparser::parser::Parser;
+use sqlparser::test_utils::{expr_from_projection, only};
#[test]
fn test_method_overrides() {
@@ -121,3 +125,68 @@ fn test_identifier_quote_style_overrides() {
None
);
}
+
+#[test]
+fn test_lambda_keyword_syntax_with_json_arrow_operator() {
+ // A custom dialect can opt into the `LAMBDA` keyword spelling of lambda
+ // functions without giving up `->` as JSON member access. The two meet in
+ // a single expression below: a lambda whose body is a JSON access.
+ //
+ // PostgreSqlDialect is used only as a convenient base that already gives
+ // `->` its JSON meaning; nothing here is specific to PostgreSQL.
+ derive_dialect!(
+ LambdaPostgreSqlDialect,
+ PostgreSqlDialect,
+ overrides = { supports_lambda_keyword_syntax = true }
+ );
+ let dialect = LambdaPostgreSqlDialect::new();
+
+ // Only the keyword spelling is enabled; the arrow spelling stays off.
+ assert!(dialect.supports_lambda_keyword_syntax());
+ assert!(!dialect.supports_lambda_functions());
+
+ let sql = "SELECT transform(xs, lambda x : (x -> 'a')::INT + 1)";
+ let ast = Parser::parse_sql(&dialect, sql).unwrap();
+ assert_eq!(sql, ast[0].to_string());
+
+ // Round-tripping alone would not distinguish a JSON access from a nested
+ // lambda, since both print as `x -> 'a'`, so check the parsed shape.
+ let Statement::Query(query) = &ast[0] else {
+ panic!("unexpected statement {}", ast[0]);
+ };
+ let Expr::Function(func) =
+ expr_from_projection(only(&query.body.as_select().unwrap().projection))
+ else {
+ panic!("expected a function call");
+ };
+ let FunctionArguments::List(args) = &func.args else {
+ panic!("expected an argument list");
+ };
+ let [_, FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Lambda(lambda)))]
= &args.args[..]
+ else {
+ panic!("expected the second argument to be a lambda");
+ };
+
+ // The lambda came from the `LAMBDA` keyword, not from `->`.
+ assert_eq!(LambdaSyntax::LambdaKeyword, lambda.syntax);
+
+ // And the `->` in its body is still JSON member access.
+ let Expr::BinaryOp {
+ left,
+ op: BinaryOperator::Plus,
+ ..
+ } = lambda.body.as_ref()
+ else {
+ panic!("expected the lambda body to be an addition");
+ };
+ let Expr::Cast { expr, .. } = left.as_ref() else {
+ panic!("expected the left operand to be a cast");
+ };
+ let Expr::Nested(json_access) = expr.as_ref() else {
+ panic!("expected the cast operand to be parenthesized");
+ };
+ let Expr::BinaryOp { op, .. } = json_access.as_ref() else {
+ panic!("expected `->` to stay a binary operator");
+ };
+ assert_eq!(&BinaryOperator::Arrow, op);
+}
diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs
index a338ef7a..51bc77fe 100644
--- a/tests/sqlparser_duckdb.rs
+++ b/tests/sqlparser_duckdb.rs
@@ -902,6 +902,21 @@ fn test_duckdb_lambda_function() {
let sql_arrow = "SELECT list_filter([1, 2, 3], x -> x > 1)";
duckdb().verified_stmt(sql_arrow);
+ // Both readings of `->` print identically, so round-tripping cannot tell
+ // a lambda from JSON member access. Assert the shape instead.
+ let select = duckdb().verified_only_select(sql_arrow);
+ let Expr::Function(func) = expr_from_projection(only(&select.projection))
else {
+ panic!("expected a function call");
+ };
+ let FunctionArguments::List(args) = &func.args else {
+ panic!("expected an argument list");
+ };
+ let [_, FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Lambda(lambda)))]
= &args.args[..]
+ else {
+ panic!("expected the second argument to be a lambda");
+ };
+ assert_eq!(LambdaSyntax::Arrow, lambda.syntax);
+
// Test lambda with multiple parameters (with index)
let sql_multi = "SELECT list_filter([1, 3, 1, 5], lambda x, i : x > i)";
duckdb().verified_stmt(sql_multi);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]