diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a1adfe59bc..38cfcfa7bf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -276,7 +276,7 @@ impl ParserOptions { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] enum ParserState { /// The default state of the parser. Normal, @@ -355,9 +355,16 @@ pub struct Parser<'a> { /// `parse_table_factor`. See [`Parser::parse_table_factor`] for the 2^N /// pattern this guards. failed_derived_table_factor_positions: BTreeSet, + /// Cached failures of the speculative `POSITION( IN )` arm. + /// See [`Parser::parse_position_expr`] for the 2^N pattern this guards. + /// Keyed by state too, since `PRIOR` parses differently in `CONNECT BY`. + failed_position_syntax_positions: BTreeSet<(usize, ParserState)>, /// 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 @@ -404,7 +411,9 @@ impl<'a> Parser<'a> { failed_prefix_positions: BTreeMap::new(), failed_reserved_word_prefix_positions: BTreeMap::new(), failed_derived_table_factor_positions: BTreeSet::new(), + failed_position_syntax_positions: BTreeSet::new(), failed_in_subquery_positions: BTreeSet::new(), + failed_lambda_positions: BTreeSet::new(), } } @@ -473,7 +482,9 @@ impl<'a> Parser<'a> { self.failed_prefix_positions.clear(); self.failed_reserved_word_prefix_positions.clear(); 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 } @@ -2413,7 +2424,14 @@ impl<'a> Parser<'a> { if !self.dialect.supports_lambda_functions() { return Ok(None); } - self.maybe_parse(|p| { + // 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| { let params = p.parse_comma_separated(|p| p.parse_lambda_function_parameter())?; p.expect_token(&Token::RParen)?; p.expect_token(&Token::Arrow)?; @@ -2423,7 +2441,11 @@ 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. @@ -3022,6 +3044,12 @@ impl<'a> Parser<'a> { /// Parse a `POSITION` expression. pub fn parse_position_expr(&mut self, ident: Ident) -> Result { let between_prec = self.dialect.prec_value(Precedence::Between); + // Memoize a failed `IN` form: the function-call fallback re-parses the + // same argument, so nested `POSITION(POSITION(...))` would take 2^N. + let memo_key = (self.index, self.state); + if self.failed_position_syntax_positions.contains(&memo_key) { + return self.parse_function(ObjectName::from(vec![ident])); + } let position_expr = self.maybe_parse(|p| { // PARSE SELECT POSITION('@' in field) p.expect_token(&Token::LParen)?; @@ -3040,7 +3068,10 @@ impl<'a> Parser<'a> { Some(expr) => Ok(expr), // Snowflake supports `position` as an ordinary function call // without the special `IN` syntax. - None => self.parse_function(ObjectName::from(vec![ident])), + None => { + self.failed_position_syntax_positions.insert(memo_key); + self.parse_function(ObjectName::from(vec![ident])) + } } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 31524da26e..5751b7d010 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -20095,6 +20095,62 @@ 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] @@ -20123,3 +20179,54 @@ fn parse_in_subquery_vs_list_dispatch() { assert_eq!(rendered.contains("InList"), !expects_subquery, "{sql}"); } } + +#[test] +fn parse_position_chain_no_exponential_blowup() { + use std::sync::mpsc; + use std::thread; + use std::time::Duration; + + let sql = String::from("SELECT ") + &"POSITION(".repeat(24) + "1" + &")".repeat(24); + + let (tx, rx) = mpsc::channel(); + // 24 levels overflow the default 2 MB test-thread stack in debug builds. + thread::Builder::new() + .stack_size(8 * 1024 * 1024) + .spawn(move || { + let _ = tx.send(Parser::parse_sql(&GenericDialect {}, &sql).map(|_| ())); + }) + .unwrap(); + + let result = rx + .recv_timeout(Duration::from_secs(5)) + .expect("parser should finish quickly, not loop exponentially"); + assert!(result.is_ok(), "{result:?}"); +} + +#[test] +fn parse_position_forms_with_nested_fallback() { + let dialects = all_dialects(); + assert!(matches!( + dialects.verified_expr("POSITION('@' IN field)"), + Expr::Position { .. } + )); + // The inner call takes the `IN` form and the outer one falls back. + match dialects.verified_expr("POSITION(POSITION('@' IN field))") { + Expr::Function(Function { + args: FunctionArguments::List(FunctionArgumentList { args, .. }), + .. + }) => assert!(matches!( + &args[..], + [FunctionArg::Unnamed(FunctionArgExpr::Expr( + Expr::Position { .. } + ))] + )), + other => panic!("outer POSITION should be an ordinary call, got {other:?}"), + } + // The outer call takes the `IN` form around an ordinary inner call. + assert!(matches!( + dialects.verified_expr("POSITION(POSITION(x) IN field)"), + Expr::Position { .. } + )); + dialects.verified_expr("POSITION(POSITION(POSITION(x)), POSITION(y))"); +}