This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-2458-d6afa0146ce929c1a82e175e613ba78d1e6673eb
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git

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]

Reply via email to