Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ 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<usize>,
/// 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<usize>,
}

/// Copy marker for a [`ParserError`] cached by the `parse_prefix` failure
Expand Down Expand Up @@ -419,6 +422,7 @@ impl<'a> Parser<'a> {
failed_prefix_positions: BTreeMap::new(),
failed_reserved_word_prefix_positions: BTreeMap::new(),
failed_derived_table_factor_positions: BTreeSet::new(),
failed_in_subquery_positions: BTreeSet::new(),
}
}

Expand Down Expand Up @@ -483,6 +487,7 @@ 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_in_subquery_positions.clear();
self
}

Expand Down Expand Up @@ -4417,7 +4422,22 @@ impl<'a> Parser<'a> {
});
}
self.expect_token(&Token::LParen)?;
let in_op = match self.maybe_parse(|p| p.parse_query())? {
// Memoize failures to break the 2^N work on inputs like `SELECT NOT IN(...`, where
// the list fallback recurses back into `parse_in` over the same tail and re-attempts
// the identical speculative parse.
let subquery_pos = self.index;
let subquery = if self.failed_in_subquery_positions.contains(&subquery_pos) {
None
} else {
match self.maybe_parse(|p| p.parse_query())? {
Some(subquery) => Some(subquery),
None => {
self.failed_in_subquery_positions.insert(subquery_pos);
None
}
}
};
let in_op = match subquery {
Some(subquery) => Expr::InSubquery {
expr: Box::new(expr),
subquery,
Expand Down
51 changes: 51 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19011,3 +19011,54 @@ fn parse_function_arg_call_chain_no_exponential_blowup() {
rx.recv_timeout(Duration::from_secs(5))
.expect("parser should reject this quickly, not loop exponentially");
}

/// A chain of `IN (` used to cost 2^depth: ~300 bytes took over 20 seconds.
#[test]
fn parse_in_chain_no_exponential_blowup() {
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

let sql = "SELECT NOT IN(\n".repeat(40);

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let _ = Parser::new(&GenericDialect {})
.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");
}

/// Asserting only that these parse would miss a silent `InSubquery` -> `InList` change,
/// since both succeed.
#[test]
fn parse_in_subquery_vs_list_dispatch() {
let cases = [
("SELECT 1 WHERE x IN (SELECT a FROM t)", true),
(
"SELECT 1 WHERE x IN (WITH c AS (SELECT 1) SELECT * FROM c)",
true,
),
("SELECT 1 WHERE x IN (VALUES (1))", true),
("SELECT 1 WHERE x IN (SELECT (1))", true),
("SELECT 1 WHERE x IN (1, 2, 3)", false),
("SELECT 1 WHERE x IN ((1), (2))", false),
("SELECT 1 WHERE x IN (select)", false),
("SELECT 1 WHERE x IN (select())", false),
("SELECT 1 WHERE x IN (select.col)", false),
];
for (sql, expects_subquery) in cases {
let ast = Parser::new(&GenericDialect {})
.try_with_sql(sql)
.and_then(|mut p| p.parse_statements())
.unwrap_or_else(|e| panic!("{sql} should parse, got {e}"));
let rendered = format!("{ast:?}");
assert_eq!(rendered.contains("InSubquery"), expects_subquery, "{sql}");
assert_eq!(rendered.contains("InList"), !expects_subquery, "{sql}");
}
}
Loading