Skip to content

Commit be95cf6

Browse files
Parser: fix exponential parse time on expression-named function arguments (apache#2392)
1 parent b478611 commit be95cf6

3 files changed

Lines changed: 115 additions & 50 deletions

File tree

sqlparser_bench/benches/sqlparser_bench.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,30 @@ fn parse_table_factor_paren_chain(c: &mut Criterion) {
273273
group.finish();
274274
}
275275

276+
/// Benchmark parsing pathological `CAST(CASE (CAST(CASE (...` chains that
277+
/// previously caused 2^N work in `parse_function_args` on dialects with
278+
/// expression-named function arguments (the argument expression was parsed
279+
/// once to detect the named form, then re-parsed on the unnamed path).
280+
fn parse_function_arg_call_chain(c: &mut Criterion) {
281+
let mut group = c.benchmark_group("parse_function_arg_call_chain");
282+
let dialect = PostgreSqlDialect {};
283+
284+
for &n in &[10usize, 20, 30] {
285+
let sql = String::from("SELECT ") + &"CAST(CASE (".repeat(n) + &")".repeat(n);
286+
287+
group.bench_function(format!("chain_{n}"), |b| {
288+
b.iter(|| {
289+
let _ = Parser::new(&dialect)
290+
.with_recursion_limit(256)
291+
.try_with_sql(std::hint::black_box(&sql))
292+
.and_then(|mut p| p.parse_statements());
293+
});
294+
});
295+
}
296+
297+
group.finish();
298+
}
299+
276300
criterion_group!(
277301
benches,
278302
basic_queries,
@@ -282,6 +306,7 @@ criterion_group!(
282306
parse_compound_keyword_chain,
283307
parse_prefix_keyword_call_chain,
284308
parse_prefix_case_chain,
285-
parse_table_factor_paren_chain
309+
parse_table_factor_paren_chain,
310+
parse_function_arg_call_chain
286311
);
287312
criterion_main!(benches);

src/parser/mod.rs

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18676,37 +18676,59 @@ impl<'a> Parser<'a> {
1867618676

1867718677
/// Parse a single function argument, handling named and unnamed variants.
1867818678
pub fn parse_function_args(&mut self) -> Result<FunctionArg, ParserError> {
18679-
let arg = if self.dialect.supports_named_fn_args_with_expr_name() {
18680-
self.maybe_parse(|p| {
18681-
let name = p.parse_expr()?;
18682-
let operator = p.parse_function_named_arg_operator()?;
18683-
let arg = p.parse_wildcard_expr()?.into();
18684-
Ok(FunctionArg::ExprNamed {
18685-
name,
18686-
arg,
18687-
operator,
18688-
})
18689-
})?
18690-
} else {
18691-
self.maybe_parse(|p| {
18692-
let name = p.parse_identifier()?;
18693-
let operator = p.parse_function_named_arg_operator()?;
18694-
let arg = p.parse_wildcard_expr()?.into();
18695-
Ok(FunctionArg::Named {
18696-
name,
18697-
arg,
18698-
operator,
18699-
})
18700-
})?
18701-
};
18679+
// Parse the argument expression once, then check for a named-arg
18680+
// operator. Parsing it speculatively and re-parsing on the unnamed
18681+
// path is O(2^depth) on nested calls like `CAST(CASE (CAST(CASE (…`.
18682+
if self.dialect.supports_named_fn_args_with_expr_name() {
18683+
let expr = self.parse_wildcard_expr()?;
18684+
// A wildcard is never a named-arg name; only the unnamed form applies.
18685+
if !matches!(expr, Expr::Wildcard(_) | Expr::QualifiedWildcard(..)) {
18686+
if let Some(operator) =
18687+
self.maybe_parse(|p| p.parse_function_named_arg_operator())?
18688+
{
18689+
let arg = self.parse_wildcard_expr()?.into();
18690+
return Ok(FunctionArg::ExprNamed {
18691+
name: expr,
18692+
arg,
18693+
operator,
18694+
});
18695+
}
18696+
}
18697+
let arg_expr = self.function_arg_expr_from_wildcard(expr)?;
18698+
return Ok(FunctionArg::Unnamed(
18699+
self.maybe_parse_aliased_function_arg(arg_expr)?,
18700+
));
18701+
}
18702+
18703+
let arg = self.maybe_parse(|p| {
18704+
let name = p.parse_identifier()?;
18705+
let operator = p.parse_function_named_arg_operator()?;
18706+
let arg = p.parse_wildcard_expr()?.into();
18707+
Ok(FunctionArg::Named {
18708+
name,
18709+
arg,
18710+
operator,
18711+
})
18712+
})?;
1870218713
if let Some(arg) = arg {
1870318714
return Ok(arg);
1870418715
}
1870518716
let wildcard_expr = self.parse_wildcard_expr()?;
18706-
let arg_expr: FunctionArgExpr = match wildcard_expr {
18717+
let arg_expr = self.function_arg_expr_from_wildcard(wildcard_expr)?;
18718+
Ok(FunctionArg::Unnamed(
18719+
self.maybe_parse_aliased_function_arg(arg_expr)?,
18720+
))
18721+
}
18722+
18723+
/// Wrap an already-parsed expression as a function argument, parsing any
18724+
/// trailing wildcard options (e.g. Snowflake's `HASH(* EXCLUDE(col))`).
18725+
fn function_arg_expr_from_wildcard(
18726+
&mut self,
18727+
wildcard_expr: Expr,
18728+
) -> Result<FunctionArgExpr, ParserError> {
18729+
Ok(match wildcard_expr {
1870718730
Expr::Wildcard(ref token) if self.dialect.supports_select_wildcard_exclude() => {
18708-
// Support `* EXCLUDE(col1, col2, ...)` inside function calls (e.g. Snowflake's
18709-
// `HASH(* EXCLUDE(col))`). Parse the options the same way SELECT items do.
18731+
// Parse the options the same way SELECT items do.
1871018732
let opts = self.parse_wildcard_additional_options(token.0.clone())?;
1871118733
if opts.opt_exclude.is_some()
1871218734
|| opts.opt_except.is_some()
@@ -18720,9 +18742,16 @@ impl<'a> Parser<'a> {
1872018742
}
1872118743
}
1872218744
other => other.into(),
18723-
};
18724-
// Aliased argument, e.g. `XMLFOREST(a AS x)` in PostgreSQL
18725-
let arg_expr = match arg_expr {
18745+
})
18746+
}
18747+
18748+
/// Parse an optional `AS <alias>` on an unnamed function argument
18749+
/// (e.g. `XMLFOREST(a AS x)` in PostgreSQL).
18750+
fn maybe_parse_aliased_function_arg(
18751+
&mut self,
18752+
arg_expr: FunctionArgExpr,
18753+
) -> Result<FunctionArgExpr, ParserError> {
18754+
Ok(match arg_expr {
1872618755
FunctionArgExpr::Expr(expr)
1872718756
if self.dialect.supports_aliased_function_args()
1872818757
&& self.parse_keyword(Keyword::AS) =>
@@ -18733,8 +18762,7 @@ impl<'a> Parser<'a> {
1873318762
})
1873418763
}
1873518764
other => other,
18736-
};
18737-
Ok(FunctionArg::Unnamed(arg_expr))
18765+
})
1873818766
}
1873918767

1874018768
fn parse_function_named_arg_operator(&mut self) -> Result<FunctionArgOperator, ParserError> {

tests/sqlparser_common.rs

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15609,11 +15609,10 @@ fn parse_create_table_select() {
1560915609

1561015610
#[test]
1561115611
fn test_reserved_keywords_for_identifiers() {
15612-
let dialects = all_dialects_where(|d| {
15613-
d.is_reserved_for_identifier(Keyword::INTERVAL)
15614-
&& !d.supports_named_fn_args_with_expr_name()
15615-
});
15616-
// Dialects that reserve the word INTERVAL will not allow it as an unquoted identifier
15612+
// Dialects that reserve INTERVAL will not allow it as an unquoted
15613+
// identifier, and report the failure consistently at the token that fails
15614+
// to start an expression (`)`), independent of named-argument support.
15615+
let dialects = all_dialects_where(|d| d.is_reserved_for_identifier(Keyword::INTERVAL));
1561715616
let sql = "SELECT MAX(interval) FROM tbl";
1561815617
assert_eq!(
1561915618
dialects.parse_sql_statements(sql),
@@ -15622,19 +15621,6 @@ fn test_reserved_keywords_for_identifiers() {
1562215621
))
1562315622
);
1562415623

15625-
// Dialects with expression-named function arguments parse the argument
15626-
// expression twice, so the second attempt reports the memoized failure
15627-
// at the start of the expression
15628-
let dialects = all_dialects_where(|d| {
15629-
d.is_reserved_for_identifier(Keyword::INTERVAL) && d.supports_named_fn_args_with_expr_name()
15630-
});
15631-
assert_eq!(
15632-
dialects.parse_sql_statements(sql),
15633-
Err(ParserError::ParserError(
15634-
"Expected: an expression, found: interval".to_string()
15635-
))
15636-
);
15637-
1563815624
// Dialects that do not reserve the word INTERVAL will allow it
1563915625
let dialects = all_dialects_where(|d| !d.is_reserved_for_identifier(Keyword::INTERVAL));
1564015626
let sql = "SELECT MAX(interval) FROM tbl";
@@ -19572,3 +19558,29 @@ fn parse_unlogged_table_logging_controls_in_all_dialects() {
1957219558
_ => unreachable!("Expected ALTER TABLE"),
1957319559
}
1957419560
}
19561+
19562+
/// Regression test for the 2^N parse-time blowup in `parse_function_args` on
19563+
/// inputs like `CAST(CASE (CAST(CASE (...`. On dialects with expression-named
19564+
/// function arguments (e.g. PostgreSQL), the named-arg arm parsed the whole
19565+
/// argument expression and then re-parsed it on the unnamed path, doubling
19566+
/// work per level. Post-fix the leading expression is parsed exactly once.
19567+
#[test]
19568+
fn parse_function_arg_call_chain_no_exponential_blowup() {
19569+
use std::sync::mpsc;
19570+
use std::thread;
19571+
use std::time::Duration;
19572+
19573+
let sql = String::from("SELECT ") + &"CAST(CASE (".repeat(30) + &")".repeat(30);
19574+
19575+
let (tx, rx) = mpsc::channel();
19576+
thread::spawn(move || {
19577+
let _ = Parser::new(&PostgreSqlDialect {})
19578+
.with_recursion_limit(256)
19579+
.try_with_sql(&sql)
19580+
.and_then(|mut p| p.parse_statements());
19581+
let _ = tx.send(());
19582+
});
19583+
19584+
rx.recv_timeout(Duration::from_secs(5))
19585+
.expect("parser should reject this quickly, not loop exponentially");
19586+
}

0 commit comments

Comments
 (0)