diff --git a/fuzz/fuzz_seeds/snowflake_2440_parens_less_nested_joins.sql b/fuzz/fuzz_seeds/snowflake_2440_parens_less_nested_joins.sql new file mode 100644 index 0000000000..50797b852a --- /dev/null +++ b/fuzz/fuzz_seeds/snowflake_2440_parens_less_nested_joins.sql @@ -0,0 +1 @@ +SELECT * FROM t JOIN t JOIN t JOIN t ON true ON true ON true diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f86406fb26..4775824b83 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17156,6 +17156,7 @@ impl<'a> Parser<'a> { && !natural && self.peek_parens_less_nested_join() { + let _guard = self.recursion_counter.try_decrease()?; let joins = self.parse_joins()?; relation = TableFactor::NestedJoin { table_with_joins: Box::new(TableWithJoins { relation, joins }), @@ -18123,18 +18124,14 @@ impl<'a> Parser<'a> { } fn parse_pattern(&mut self) -> Result { - let pattern = self.parse_concat_pattern()?; - if self.consume_token(&Token::Pipe) { - match self.parse_pattern()? { - // flatten nested alternations - MatchRecognizePattern::Alternation(mut patterns) => { - patterns.insert(0, pattern); - Ok(MatchRecognizePattern::Alternation(patterns)) - } - next => Ok(MatchRecognizePattern::Alternation(vec![pattern, next])), - } - } else { - Ok(pattern) + let _guard = self.recursion_counter.try_decrease()?; + let mut patterns = vec![self.parse_concat_pattern()?]; + while self.consume_token(&Token::Pipe) { + patterns.push(self.parse_concat_pattern()?); + } + match <[MatchRecognizePattern; 1]>::try_from(patterns) { + Ok([pattern]) => Ok(pattern), + Err(patterns) => Ok(MatchRecognizePattern::Alternation(patterns)), } } @@ -18189,6 +18186,7 @@ impl<'a> Parser<'a> { /// Parses MySQL's JSON_TABLE column definition. /// For example: `id INT EXISTS PATH '$' DEFAULT '0' ON EMPTY ERROR ON ERROR` pub fn parse_json_table_column_def(&mut self) -> Result { + let _guard = self.recursion_counter.try_decrease()?; if self.parse_keyword(Keyword::NESTED) { let _has_path_keyword = self.parse_keyword(Keyword::PATH); let path = self.parse_value()?; @@ -22104,6 +22102,7 @@ impl<'a> Parser<'a> { parenthesized: bool, end_words: &[Keyword], ) -> Result { + let _guard = self.recursion_counter.try_decrease()?; let mut options: Vec = Vec::new(); let mut delimiter = KeyValueOptionsDelimiter::Space; if parenthesized { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 379e37a660..fdf18a2654 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -22744,3 +22744,64 @@ fn display_create_procedure_parameter_list_propagates_write_errors() { let mut writer = RejectingWriter(" ("); assert_eq!(write!(writer, "{statement}"), Err(fmt::Error)); } + +#[test] +fn parse_deeply_nested_match_recognize_pattern_hits_recursion_limits() { + let dialect = GenericDialect {}; + + let sql = format!( + "SELECT * FROM t MATCH_RECOGNIZE(PATTERN ({}a{}) DEFINE a AS true)", + "(".repeat(1000), + ")".repeat(1000) + ); + + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn parse_deeply_nested_json_table_columns_hits_recursion_limits() { + let dialect = GenericDialect {}; + + let sql = format!( + "SELECT * FROM JSON_TABLE('{{}}', '$' COLUMNS({}a INT PATH '$'{}))", + "NESTED PATH '$' COLUMNS(".repeat(1000), + ")".repeat(1000) + ); + + let res = Parser::new(&dialect) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); +} + +#[test] +fn parse_match_recognize_pattern_alternation_within_recursion_limits() { + let sql = format!( + "SELECT * FROM t MATCH_RECOGNIZE(PATTERN ({}) DEFINE a AS 1 = 1)", + vec!["a"; 100].join(" | ") + ); + all_dialects_where(|d| d.supports_match_recognize()).verified_stmt(&sql); +} + +#[test] +fn parse_nested_parenthesized_joins_within_recursion_limits() { + let sql = format!( + "SELECT * FROM {}u{}", + "(t JOIN ".repeat(6), + " ON true)".repeat(6) + ); + // A lower limit keeps the debug-build stack within a test thread's default. + let res = Parser::new(&GenericDialect {}) + .with_recursion_limit(10) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + assert!(res.is_ok(), "{res:?}"); +} diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5e3cbc8e01..b4f6a9365e 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -23,7 +23,7 @@ use sqlparser::ast::helpers::key_value_options::{KeyValueOption, KeyValueOptionK use sqlparser::ast::helpers::stmt_data_loading::{StageLoadSelectItem, StageLoadSelectItemKind}; use sqlparser::ast::*; use sqlparser::dialect::{Dialect, GenericDialect, SnowflakeDialect}; -use sqlparser::parser::{ParserError, ParserOptions}; +use sqlparser::parser::{Parser, ParserError, ParserOptions}; use sqlparser::tokenizer::*; use test_utils::*; @@ -5042,3 +5042,28 @@ fn test_stage_name_delimiters() { ParserError::ParserError("Expected: stage name identifier, found: .".to_string()), ); } + +#[test] +fn test_deeply_nested_key_value_options_hit_recursion_limits() { + let sql = format!( + "CREATE STAGE s COPY_OPTIONS=({}b=1{})", + "a=(".repeat(1000), + ")".repeat(1000) + ); + let res = snowflake().parse_sql_statements(&sql); + assert_eq!(ParserError::RecursionLimitExceeded, res.unwrap_err()); +} + +#[test] +fn test_parens_less_nested_joins_hit_recursion_limits() { + let sql = format!("SELECT * FROM t{}", " JOIN t".repeat(1000)); + // A lower limit keeps the debug-build stack within a test thread's default. + let res = Parser::new(&SnowflakeDialect {}) + .with_recursion_limit(10) + .try_with_sql(&sql) + .expect("tokenize to work") + .parse_statements(); + assert_eq!(res, Err(ParserError::RecursionLimitExceeded)); + + snowflake().verified_stmt("SELECT * FROM t JOIN t2 ON true JOIN t3 ON true"); +}