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/* 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 diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 16583eb449..2af8b620c6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -362,7 +362,7 @@ 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 + /// Cached failures of the speculative `() -> ` arm. See /// [`Parser::try_parse_lambda`] for the 2^N pattern this guards. failed_lambda_positions: BTreeSet<(usize, ParserState)>, } @@ -2432,11 +2432,10 @@ 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) { + // 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| { @@ -2451,7 +2450,7 @@ impl<'a> Parser<'a> { })) })?; if lambda.is_none() { - self.failed_lambda_positions.insert(lambda_pos); + self.failed_lambda_positions.insert(memo_key); } Ok(lambda) } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index b417db64ff..684180f9c6 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] @@ -20293,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() + )) + ); +}