From ea5645aa9a845b7584432d382a3c8530ba3e1750 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Tue, 20 Oct 2026 15:18:25 -0700 Subject: [PATCH 1/4] Revert the fork's own fix for `(x) ->` lambda chains (#18) Upstream #2699 replaces it. The cherry-pick of upstream #2619 for nested POSITION calls, also in #18, stays. --- src/parser/mod.rs | 20 ++------------ tests/sqlparser_common.rs | 56 --------------------------------------- 2 files changed, 2 insertions(+), 74 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16583eb449..9a47065aca 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -362,9 +362,6 @@ pub struct Parser<'a> { /// Cached failures from the speculative subquery arm of `parse_in`. See /// [`Parser::parse_in`] for the 2^N pattern this guards. failed_in_subquery_positions: BTreeSet, - /// Cached failures from the speculative arm of `try_parse_lambda`. See - /// [`Parser::try_parse_lambda`] for the 2^N pattern this guards. - failed_lambda_positions: BTreeSet<(usize, ParserState)>, } /// Copy marker for a [`ParserError`] cached by the `parse_prefix` failure @@ -413,7 +410,6 @@ impl<'a> Parser<'a> { failed_derived_table_factor_positions: BTreeSet::new(), failed_position_syntax_positions: BTreeSet::new(), failed_in_subquery_positions: BTreeSet::new(), - failed_lambda_positions: BTreeSet::new(), } } @@ -484,7 +480,6 @@ 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 } @@ -2432,14 +2427,7 @@ impl<'a> Parser<'a> { if !self.dialect.supports_lambda_functions() { return Ok(None); } - // Memoize failures to break the 2^N work on inputs like `(x)->(x)->...` with a body - // that fails to parse: the fallback parses `(x)` as a nested expression and `->` as a - // binary operator, whose right side re-attempts the identical speculative parse. - let lambda_pos = (self.index, self.state); - if self.failed_lambda_positions.contains(&lambda_pos) { - return Ok(None); - } - let lambda = self.maybe_parse(|p| { + 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,11 +2437,7 @@ impl<'a> Parser<'a> { body: Box::new(expr), syntax: LambdaSyntax::Arrow, })) - })?; - if lambda.is_none() { - self.failed_lambda_positions.insert(lambda_pos); - } - Ok(lambda) + }) } /// Parses a lambda expression following the `LAMBDA` keyword syntax. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b417db64ff..2b47428b2e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20158,62 +20158,6 @@ fn parse_in_chain_no_exponential_blowup() { .expect("parser should reject this quickly, not loop exponentially"); } -/// A chain of `(x)->` whose last body fails to parse used to cost 2^depth: each lambda -/// attempt fails, and its `(x) -> ...` binary-operator fallback re-attempts the next one. -#[test] -fn parse_lambda_arrow_chain_no_exponential_blowup() { - use std::sync::mpsc; - use std::thread; - use std::time::Duration; - - let sql = format!("SELECT {}1 +", "(x)->".repeat(40)); - - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - for dialect in all_dialects_where(|d| d.supports_lambda_functions()).dialects { - let _ = Parser::new(&*dialect) - .with_recursion_limit(256) - .try_with_sql(&sql) - .and_then(|mut p| p.parse_statements()); - } - let _ = tx.send(()); - }); - - rx.recv_timeout(Duration::from_secs(5)) - .expect("parser should reject this quickly, not loop exponentially"); -} - -/// A cached lambda failure must fall back exactly as a fresh one does. -#[test] -fn parse_lambda_vs_arrow_operator_dispatch() { - let dialects = all_dialects_where(|d| d.supports_lambda_functions()); - let cases = [ - ("SELECT (x) -> (y) -> x + y", 2, 0), - ("SELECT (x) -> x, (1) -> 'k'", 1, 1), - ("SELECT (1) -> (x) -> x", 1, 1), - ("SELECT ((x) -> 1) -> 'k'", 1, 1), - ]; - for (sql, lambdas, arrows) in cases { - for dialect in &dialects.dialects { - let ast = Parser::new(&**dialect) - .try_with_sql(sql) - .and_then(|mut p| p.parse_statements()) - .unwrap_or_else(|e| panic!("{sql} should parse under {dialect:?}, got {e}")); - let rendered = format!("{ast:?}"); - assert_eq!( - rendered.matches("Lambda(").count(), - lambdas, - "{sql} under {dialect:?}" - ); - assert_eq!( - rendered.matches("op: Arrow").count(), - arrows, - "{sql} under {dialect:?}" - ); - } - } -} - /// Asserting only that these parse would miss a silent `InSubquery` -> `InList` change, /// since both succeed. #[test] From a458830ac92b71e11166a45caad25ecfa9b3d89e Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Mon, 19 Oct 2026 17:33:49 -0700 Subject: [PATCH 2/4] 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 9a47065aca..2af8b620c6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -362,6 +362,9 @@ pub struct Parser<'a> { /// Cached failures from the speculative subquery arm of `parse_in`. See /// [`Parser::parse_in`] for the 2^N pattern this guards. failed_in_subquery_positions: BTreeSet, + /// Cached failures of the speculative `() -> ` arm. See + /// [`Parser::try_parse_lambda`] for the 2^N pattern this guards. + failed_lambda_positions: BTreeSet<(usize, ParserState)>, } /// Copy marker for a [`ParserError`] cached by the `parse_prefix` failure @@ -410,6 +413,7 @@ impl<'a> Parser<'a> { failed_derived_table_factor_positions: BTreeSet::new(), failed_position_syntax_positions: BTreeSet::new(), failed_in_subquery_positions: BTreeSet::new(), + failed_lambda_positions: BTreeSet::new(), } } @@ -480,6 +484,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 } @@ -2427,7 +2432,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)?; @@ -2437,7 +2448,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 2b47428b2e..684180f9c6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20237,3 +20237,69 @@ fn parse_position_forms_with_nested_fallback() { )); dialects.verified_expr("POSITION(POSITION(POSITION(x)), POSITION(y))"); } + +#[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 97a582ddef1cc37c54b50b68b77fb854747506a3 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Tue, 20 Oct 2026 15:16:17 -0700 Subject: [PATCH 3/4] 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 From 7ff4d822036fdd0bc935730dc8e1a9e3dede9915 Mon Sep 17 00:00:00 2001 From: Mosha Pasumansky Date: Thu, 22 Oct 2026 13:11:56 -0700 Subject: [PATCH 4/4] Exclude fuzz seeds from the RAT check, as upstream does The seed added with the #2699 port is the first file under fuzz/fuzz_seeds on this branch; upstream main already excludes the directory. --- dev/release/rat_exclude_files.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt index b8b7b81d08..3ec3e51a99 100644 --- a/dev/release/rat_exclude_files.txt +++ b/dev/release/rat_exclude_files.txt @@ -6,4 +6,5 @@ dev/release/rat_exclude_files.txt sqlparser_bench/img/flamegraph.svg **Cargo.lock filtered_rat.txt -AGENTS.md \ No newline at end of file +AGENTS.md +fuzz/fuzz_seeds/*