Skip to content

Commit feb12da

Browse files
Parser: add regression test and bench for named-arg chain blowup
1 parent 2705a7d commit feb12da

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

sqlparser_bench/benches/sqlparser_bench.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// under the License.
1717

1818
use criterion::{criterion_group, criterion_main, Criterion};
19-
use sqlparser::dialect::GenericDialect;
19+
use sqlparser::dialect::{GenericDialect, PostgreSqlDialect};
2020
use sqlparser::keywords::Keyword;
2121
use sqlparser::parser::Parser;
2222
use sqlparser::tokenizer::{Span, Word};
@@ -177,11 +177,38 @@ fn parse_compound_chain(c: &mut Criterion) {
177177
group.finish();
178178
}
179179

180+
/// Benchmark parsing pathological named-arg chains that previously caused
181+
/// 2^N work in `parse_function_args` on dialects with expression-named
182+
/// function arguments (PostgreSQL, MSSQL). Each `--<newline>` swallows the
183+
/// trailing `,i)`, leaving a chain of unterminated function calls that the
184+
/// previous `maybe_parse(parse_expr)` re-walked on rollback.
185+
fn parse_named_arg_chain(c: &mut Criterion) {
186+
let mut group = c.benchmark_group("parse_named_arg_chain");
187+
let dialect = PostgreSqlDialect {};
188+
189+
for &n in &[5usize, 10, 15] {
190+
let body = std::iter::repeat(".foo(t--,i)")
191+
.take(n)
192+
.collect::<Vec<_>>()
193+
.join("\n");
194+
let sql = format!("SELECT Y\n{body}");
195+
196+
group.bench_function(format!("chain_{n}"), |b| {
197+
b.iter(|| {
198+
let _ = Parser::parse_sql(&dialect, std::hint::black_box(&sql));
199+
});
200+
});
201+
}
202+
203+
group.finish();
204+
}
205+
180206
criterion_group!(
181207
benches,
182208
basic_queries,
183209
word_to_ident,
184210
parse_many_identifiers,
185-
parse_compound_chain
211+
parse_compound_chain,
212+
parse_named_arg_chain
186213
);
187214
criterion_main!(benches);

tests/sqlparser_common.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19004,3 +19004,30 @@ fn parse_compound_chain_no_exponential_blowup() {
1900419004
rx.recv_timeout(Duration::from_secs(5))
1900519005
.expect("parser should reject this quickly, not loop exponentially");
1900619006
}
19007+
19008+
/// Regression test for the 2^N parse-time blowup in `parse_function_args` on
19009+
/// dialects that allow `<expr> <op> <expr>` named args (PostgreSQL, MSSQL).
19010+
/// Each `--<newline>` swallows the trailing `,i)`, leaving a chain of
19011+
/// unterminated function calls that the previous `maybe_parse(parse_expr)`
19012+
/// re-walked on rollback. Post-fix the parser returns `Err` in well under
19013+
/// a millisecond.
19014+
#[test]
19015+
fn parse_named_arg_chain_no_exponential_blowup() {
19016+
use std::sync::mpsc;
19017+
use std::thread;
19018+
use std::time::Duration;
19019+
19020+
let body: String = std::iter::repeat_n(".foo(t--,i)", 25)
19021+
.collect::<Vec<_>>()
19022+
.join("\n");
19023+
let sql = format!("SELECT Y\n{body}");
19024+
19025+
let (tx, rx) = mpsc::channel();
19026+
thread::spawn(move || {
19027+
let _ = Parser::parse_sql(&PostgreSqlDialect {}, &sql);
19028+
let _ = tx.send(());
19029+
});
19030+
19031+
rx.recv_timeout(Duration::from_secs(5))
19032+
.expect("PostgreSQL parser should reject this quickly, not loop exponentially");
19033+
}

0 commit comments

Comments
 (0)