From d88e0a5b379ae038c86ec4b5b3743cb04a4a0874 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Mon, 19 Oct 2026 17:33:49 -0700 Subject: [PATCH 1/2] Parser: fix exponential parse time on `(x) ->` lambda chains Co-Authored-By: Claude Opus 5.5 (1M context) --- src/parser/mod.rs | 19 +++++++++-- tests/sqlparser_common.rs | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d09c601eb9..43ca8b7c61 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -371,6 +371,9 @@ pub struct Parser<'a> { /// every level, taking 2^N time. Maps the arm's start to where a parsed /// query lacked its `)`. failed_in_subquery_positions: BTreeMap>, + /// Cached failures of the speculative `() -> ` arm. See + /// [`Parser::try_parse_lambda`] for the 2^N pattern this guards. + failed_lambda_positions: BTreeSet<(usize, ParserState)>, } /// Whether an alias names a select item or a table. @@ -426,6 +429,7 @@ impl<'a> Parser<'a> { failed_derived_table_factor_positions: BTreeSet::new(), failed_position_syntax_positions: BTreeSet::new(), failed_in_subquery_positions: BTreeMap::new(), + failed_lambda_positions: BTreeSet::new(), } } @@ -496,6 +500,7 @@ impl<'a> Parser<'a> { self.failed_derived_table_factor_positions.clear(); self.failed_position_syntax_positions.clear(); self.failed_in_subquery_positions.clear(); + self.failed_lambda_positions.clear(); self } @@ -2439,7 +2444,13 @@ impl<'a> Parser<'a> { if !self.dialect.supports_lambda_functions() { return Ok(None); } - self.maybe_parse(|p| { + // Memoize a failed lambda: the fallback parses `(x)` as a nested expression and `->` as + // a binary operator, so a chain `(x)->(x)->...` would retry each lambda 2^N times. + let memo_key = (self.index, self.state); + if self.failed_lambda_positions.contains(&memo_key) { + return Ok(None); + } + let lambda = self.maybe_parse(|p| { let params = p.parse_comma_separated(|p| p.parse_lambda_function_parameter())?; p.expect_token(&Token::RParen)?; p.expect_token(&Token::Arrow)?; @@ -2449,7 +2460,11 @@ impl<'a> Parser<'a> { body: Box::new(expr), syntax: LambdaSyntax::Arrow, })) - }) + })?; + if lambda.is_none() { + self.failed_lambda_positions.insert(memo_key); + } + Ok(lambda) } /// Parses a lambda expression following the `LAMBDA` keyword syntax. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7649790742..21955e6374 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22386,3 +22386,69 @@ fn parse_grant_function_signature() { ParserError::ParserError("Expected: ), found: EOF".to_string()) ); } + +#[test] +fn parse_lambda_chain_no_exponential_blowup() { + use std::sync::mpsc; + use std::thread; + use std::time::Duration; + + // Every lambda in the chain fails because the final body is incomplete. + let sql = format!("SELECT {}1 +", "(x) -> ".repeat(24)); + + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let dialects = all_dialects_where(|d| d.supports_lambda_functions()); + let _ = tx.send(dialects.dialects.iter().all(|dialect| { + Parser::new(dialect.as_ref()) + .with_recursion_limit(256) + .try_with_sql(&sql) + .and_then(|mut p| p.parse_statements()) + .is_err() + })); + }); + + let rejected = rx + .recv_timeout(Duration::from_secs(5)) + .expect("parser should finish quickly, not loop exponentially"); + assert!(rejected); +} + +#[test] +fn parse_lambda_forms_with_arrow_fallback() { + let dialects = all_dialects_where(|d| d.supports_lambda_functions()); + match dialects.verified_expr("(x) -> (y) -> x + y") { + Expr::Lambda(LambdaFunction { body, .. }) => { + assert!(matches!(*body, Expr::Lambda(_)), "{body:?}") + } + other => panic!("expected a lambda, got {other:?}"), + } + // `(1)` cannot be a parameter list, so it falls back to `->` around a lambda. + match dialects.verified_expr("(1) -> (x) -> x") { + Expr::BinaryOp { + op: BinaryOperator::Arrow, + right, + .. + } => assert!(matches!(*right, Expr::Lambda(_)), "{right:?}"), + other => panic!("expected `->` around a lambda, got {other:?}"), + } + // The lambda inside the parentheses is the left operand of `->`. + match dialects.verified_expr("((x) -> 1) -> 'k'") { + Expr::BinaryOp { + op: BinaryOperator::Arrow, + left, + .. + } => assert!( + matches!(&*left, Expr::Nested(inner) if matches!(**inner, Expr::Lambda(_))), + "{left:?}" + ), + other => panic!("expected `->` with a lambda operand, got {other:?}"), + } + // The inner lambda is retried from the cache and fails as it did uncached. + assert_eq!( + dialects.parse_sql_statements("SELECT (x) -> (x) -> 1 +"), + Err(ParserError::ParserError( + "Expected: an expression, found: EOF".to_string() + )) + ); +} From 4b69105e323acb5f3fc9a594b429e5e84ef65d14 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Tue, 20 Oct 2026 15:16:17 -0700 Subject: [PATCH 2/2] Add a fuzz seed for `(x) ->` lambda chains Co-Authored-By: Claude Opus 5.5 (1M context) --- fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql diff --git a/fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql b/fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql new file mode 100644 index 0000000000..3ffe18eea3 --- /dev/null +++ b/fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql @@ -0,0 +1 @@ +SELECT (x) -> (y) -> x + y, (1) -> (x) -> x FROM t