From 17ee034b8cc711133438175d36e5a0ce08eab9a8 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:29:11 -0500 Subject: [PATCH 1/6] Allow dialects that use `->` as an operator to support `LAMBDA` syntax --- src/dialect/mod.rs | 20 ++++++++ src/parser/mod.rs | 2 +- tests/sqlparser_custom_dialect.rs | 82 +++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ff83a4da6..c868c66be 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -535,10 +535,30 @@ 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 transform(array(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 PostgreSQL + /// and its derivatives, where `->` is JSON member access. Defaults to + /// [`Self::supports_lambda_functions`], so dialects supporting the `->` + /// spelling accept the `LAMBDA` spelling too unless they say otherwise. + 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 e7e5afa07..943099556 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1635,7 +1635,7 @@ impl<'a> Parser<'a> { Keyword::MAP if *self.peek_token_ref() == Token::LBrace && self.dialect.support_map_literal_syntax() => { Ok(Some(self.parse_duckdb_map_literal()?)) } - 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 cee604aca..8ca78982b 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,84 @@ 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]) + ); + } +} From 571b7eda64af2e8f14ae8615cf21edf6e979adb5 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:19:28 -0500 Subject: [PATCH 2/6] Remove redundant borrows in format! args to satisfy clippy 1.98 --- tests/sqlparser_custom_dialect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index 8ca78982b..aa0488601 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -196,7 +196,7 @@ fn custom_dialect_lambda_keyword_syntax_without_arrow() { let sql = "SELECT transform(xs, lambda x : x + 1)"; assert_eq!( sql, - &format!("{}", &Parser::parse_sql(&dialect, sql).unwrap()[0]) + &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0]) ); // `->` keeps whatever meaning the dialect gives it, rather than @@ -245,7 +245,7 @@ fn custom_dialect_lambda_keyword_defaults_to_arrow_support() { ] { assert_eq!( sql, - &format!("{}", &Parser::parse_sql(&dialect, sql).unwrap()[0]) + &format!("{}", Parser::parse_sql(&dialect, sql).unwrap()[0]) ); } } From ede1099ce9a46ea115d89b9f4f3edfd03609224c Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:19:05 -0500 Subject: [PATCH 3/6] Add derive_dialect! regression test for JSON access plus lambdas Exercises the capability the way a downstream crate would: derive a dialect from PostgreSqlDialect with `supports_lambda_keyword_syntax` overridden, then check that `LAMBDA x : x + 1` parses while `->` and `->>` keep parsing as JSON member access rather than lambda parameters. PostgreSqlDialect is used here only as a convenient base that already gives `->` its JSON meaning; the case being covered is any custom dialect that wants JSON accessors and lambda functions at once. Co-Authored-By: Claude Opus 5 --- tests/sqlparser_derive_dialect.rs | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/sqlparser_derive_dialect.rs b/tests/sqlparser_derive_dialect.rs index d60fa1e11..30c03edb2 100644 --- a/tests/sqlparser_derive_dialect.rs +++ b/tests/sqlparser_derive_dialect.rs @@ -17,9 +17,11 @@ //! Tests for the `derive_dialect!` macro. +use sqlparser::ast::{BinaryOperator, Expr, 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 +123,45 @@ fn test_identifier_quote_style_overrides() { None ); } + +#[test] +fn test_lambda_keyword_syntax_on_postgres_derivative() { + // A PostgreSQL derivative can opt into the `LAMBDA` keyword spelling of + // lambda functions without giving up `->` as JSON member access. + 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()); + + // The `LAMBDA` spelling parses. + let sql = "SELECT transform(xs, lambda x : x + 1)"; + let ast = Parser::parse_sql(&dialect, sql).unwrap(); + assert_eq!(sql, ast[0].to_string()); + + // `->` and `->>` still parse as JSON member access rather than + // introducing a lambda parameter. + for (sql, expected_op) in [ + ("SELECT a -> 'b'", BinaryOperator::Arrow), + ("SELECT a ->> 'b'", BinaryOperator::LongArrow), + ] { + let ast = Parser::parse_sql(&dialect, sql).unwrap(); + assert_eq!(sql, ast[0].to_string()); + match &ast[0] { + Statement::Query(query) => { + let Expr::BinaryOp { op, .. } = + expr_from_projection(only(&query.body.as_select().unwrap().projection)) + else { + panic!("expected `{sql}` to parse as a binary operator"); + }; + assert_eq!(&expected_op, op); + } + stmt => panic!("unexpected statement {stmt}"), + } + } +} From df894154957dcec86d443aa7bbf1210f6e728812 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 26 Aug 2026 07:39:08 -0500 Subject: [PATCH 4/6] Fold the lambda/JSON-access test into a single query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: rather than parsing the `LAMBDA` spelling and the `->` operator as separate statements, parse one expression that uses both — a lambda whose body is a JSON access — which is the shape a PostgreSQL derivative actually cares about. Co-Authored-By: Claude Opus 5 --- tests/sqlparser_derive_dialect.rs | 70 ++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/tests/sqlparser_derive_dialect.rs b/tests/sqlparser_derive_dialect.rs index 30c03edb2..cc7508bf1 100644 --- a/tests/sqlparser_derive_dialect.rs +++ b/tests/sqlparser_derive_dialect.rs @@ -17,7 +17,9 @@ //! Tests for the `derive_dialect!` macro. -use sqlparser::ast::{BinaryOperator, Expr, Statement}; +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; @@ -127,7 +129,8 @@ fn test_identifier_quote_style_overrides() { #[test] fn test_lambda_keyword_syntax_on_postgres_derivative() { // A PostgreSQL derivative can opt into the `LAMBDA` keyword spelling of - // lambda functions without giving up `->` as JSON member access. + // 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. derive_dialect!( LambdaPostgreSqlDialect, PostgreSqlDialect, @@ -139,29 +142,48 @@ fn test_lambda_keyword_syntax_on_postgres_derivative() { assert!(dialect.supports_lambda_keyword_syntax()); assert!(!dialect.supports_lambda_functions()); - // The `LAMBDA` spelling parses. - let sql = "SELECT transform(xs, lambda x : x + 1)"; + 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()); - // `->` and `->>` still parse as JSON member access rather than - // introducing a lambda parameter. - for (sql, expected_op) in [ - ("SELECT a -> 'b'", BinaryOperator::Arrow), - ("SELECT a ->> 'b'", BinaryOperator::LongArrow), - ] { - let ast = Parser::parse_sql(&dialect, sql).unwrap(); - assert_eq!(sql, ast[0].to_string()); - match &ast[0] { - Statement::Query(query) => { - let Expr::BinaryOp { op, .. } = - expr_from_projection(only(&query.body.as_select().unwrap().projection)) - else { - panic!("expected `{sql}` to parse as a binary operator"); - }; - assert_eq!(&expected_op, op); - } - stmt => panic!("unexpected statement {stmt}"), - } - } + // 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); } From 752b6e760ecea253ee9ec6de9a9e53955cf0d08f Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:42:57 -0500 Subject: [PATCH 5/6] Address review: DuckDB doc example and link, arrow-only dialect test All three changes are applied review suggestions from Luca Cappelletti. Co-Authored-By: Luca Cappelletti <7738570+LucaCappelletti94@users.noreply.github.com> Co-Authored-By: Claude Opus 5 --- src/dialect/mod.rs | 4 +++- tests/sqlparser_custom_dialect.rs | 35 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index c868c66be..d3c01e3a7 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -547,7 +547,7 @@ pub trait Dialect: Debug + Any { /// lambda functions, for example: /// /// ```sql - /// SELECT transform(array(1, 2, 3), LAMBDA x : x + 1); -- returns [2,3,4] + /// 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 @@ -555,6 +555,8 @@ pub trait Dialect: Debug + Any { /// and its derivatives, where `->` is JSON member access. Defaults to /// [`Self::supports_lambda_functions`], so dialects supporting the `->` /// spelling accept the `LAMBDA` spelling too unless they say otherwise. + /// + /// See fn supports_lambda_keyword_syntax(&self) -> bool { self.supports_lambda_functions() } diff --git a/tests/sqlparser_custom_dialect.rs b/tests/sqlparser_custom_dialect.rs index aa0488601..5bf38ddec 100644 --- a/tests/sqlparser_custom_dialect.rs +++ b/tests/sqlparser_custom_dialect.rs @@ -249,3 +249,38 @@ fn custom_dialect_lambda_keyword_defaults_to_arrow_support() { ); } } + +#[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()); +} From 7e7bfc75d4f7c9f2c2c7e648e9c197177a9bed45 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:56:45 -0700 Subject: [PATCH 6/6] Reframe the motivation on DuckDB and custom dialects PostgreSQL was a poor example: it has no lambda functions, so the capability was unreachable there and the motivation read as speculative. The real case is DuckDB, which uses `->` for both JSON member access and the arrow lambda form and resolves the ambiguity from the function signature at bind time. A parser cannot, which is why DuckDB deprecated the arrow form in v1.3 (`SET lambda_syntax`) and disables it by default in v2.0. Expressing that requires the two spellings to be separable. Also pin the shape of DuckDB's `x -> x > 1` as a lambda. Both readings of `->` print identically, so the existing round-trip check passes under either one; without this assertion, changing `DuckDbDialect` to the keyword-only spelling leaves the whole suite green. Co-Authored-By: Claude Opus 5 --- src/dialect/mod.rs | 13 +++++++++---- tests/sqlparser_derive_dialect.rs | 11 +++++++---- tests/sqlparser_duckdb.rs | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index d3c01e3a7..4fea38d90 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -551,10 +551,15 @@ pub trait Dialect: Debug + Any { /// ``` /// /// This spelling does not claim the `->` token, so it can be enabled by - /// dialects that already give `->` a different meaning, such as PostgreSQL - /// and its derivatives, where `->` is JSON member access. Defaults to - /// [`Self::supports_lambda_functions`], so dialects supporting the `->` - /// spelling accept the `LAMBDA` spelling too unless they say otherwise. + /// dialects that already give `->` a different meaning — for example JSON + /// member access. DuckDB uses `->` for both, resolving the ambiguity from + /// the function signature at bind time rather than while parsing, and + /// deprecated the arrow lambda form in v1.3 in favour of this one; v2.0 + /// disables the arrow form by default. + /// + /// Defaults to [`Self::supports_lambda_functions`], so dialects supporting + /// the `->` spelling accept the `LAMBDA` spelling too unless they say + /// otherwise. /// /// See fn supports_lambda_keyword_syntax(&self) -> bool { diff --git a/tests/sqlparser_derive_dialect.rs b/tests/sqlparser_derive_dialect.rs index cc7508bf1..6320556fc 100644 --- a/tests/sqlparser_derive_dialect.rs +++ b/tests/sqlparser_derive_dialect.rs @@ -127,10 +127,13 @@ fn test_identifier_quote_style_overrides() { } #[test] -fn test_lambda_keyword_syntax_on_postgres_derivative() { - // A PostgreSQL derivative 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. +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, diff --git a/tests/sqlparser_duckdb.rs b/tests/sqlparser_duckdb.rs index a338ef7a8..ff82bef9d 100644 --- a/tests/sqlparser_duckdb.rs +++ b/tests/sqlparser_duckdb.rs @@ -902,6 +902,24 @@ fn test_duckdb_lambda_function() { let sql_arrow = "SELECT list_filter([1, 2, 3], x -> x > 1)"; duckdb().verified_stmt(sql_arrow); + // `->` is ambiguous in DuckDB: it is both the arrow lambda spelling and + // JSON member access, and DuckDB resolves it from the function signature + // at bind time. `DuckDbDialect` currently resolves it to a lambda. Both + // readings print identically, so round-tripping cannot tell them apart — + // assert the shape so any future change to that choice is visible here. + 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);