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
3 changes: 2 additions & 1 deletion dev/release/rat_exclude_files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dev/release/rat_exclude_files.txt
sqlparser_bench/img/flamegraph.svg
**Cargo.lock
filtered_rat.txt
AGENTS.md
AGENTS.md
fuzz/fuzz_seeds/*
1 change: 1 addition & 0 deletions fuzz/fuzz_seeds/databricks_2699_lambda_arrow_chain.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT (x) -> (y) -> x + y, (1) -> (x) -> x FROM t
13 changes: 6 additions & 7 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize>,
/// Cached failures from the speculative arm of `try_parse_lambda`. See
/// Cached failures of the speculative `(<params>) -> <body>` arm. See
/// [`Parser::try_parse_lambda`] for the 2^N pattern this guards.
failed_lambda_positions: BTreeSet<(usize, ParserState)>,
}
Expand Down Expand Up @@ -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| {
Expand All @@ -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)
}
Expand Down
122 changes: 66 additions & 56 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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()
))
);
}
Loading