use crate::ast::dialect::FeatureSet;
use crate::ast::{NoExt, Statement};
use crate::error::{ParseErrorKind, ParseResult};
use crate::tokenizer::tokenize;
use super::{FeatureDialect, ParseOptions, Parser, TestDialect, parse_with, parse_with_options};
fn nested_parens(n: usize) -> String {
format!("SELECT {}1{}", "(".repeat(n), ")".repeat(n))
}
fn nested_paren_queries(n: usize) -> String {
format!("{}SELECT 1{}", "(".repeat(n), ")".repeat(n))
}
fn nested_scalar_subqueries(n: usize) -> String {
format!("SELECT {}1{}", "(SELECT ".repeat(n), ")".repeat(n))
}
fn nested_derived_tables(n: usize) -> String {
format!(
"SELECT * FROM {}t{}",
"(SELECT * FROM ".repeat(n),
")".repeat(n)
)
}
fn nested_join_parens(n: usize) -> String {
format!(
"SELECT * FROM {}t JOIN u ON TRUE{}",
"(".repeat(n),
")".repeat(n)
)
}
fn nested_explains(n: usize) -> String {
format!("{}SELECT 1", "EXPLAIN ".repeat(n))
}
const LOW_LIMIT: usize = 16;
fn limit(n: usize) -> ParseOptions {
ParseOptions::default().with_recursion_limit(n)
}
#[test]
fn nested_parentheses_past_the_limit_reject_cleanly() {
let sql = nested_parens(LOW_LIMIT + 8);
let err = parse_with_options(&sql, TestDialect, limit(LOW_LIMIT))
.expect_err("parentheses nested past the limit must be rejected, not crash");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
assert!(
!err.span.is_synthetic(),
"recursion error keeps a real span"
);
assert!(err.span.end() <= sql.len() as u32);
}
#[test]
fn nested_subqueries_past_the_limit_reject_cleanly() {
let sql = nested_paren_queries(LOW_LIMIT + 8);
let err = parse_with_options(&sql, TestDialect, limit(LOW_LIMIT))
.expect_err("subqueries nested past the limit must be rejected, not crash");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
}
#[test]
fn high_but_safe_nesting_is_accepted_at_the_default_limit() {
parse_with(&nested_parens(64), TestDialect).expect("64 nested parens parse under the default");
parse_with(&nested_paren_queries(64), TestDialect)
.expect("64 nested subqueries parse under the default");
}
#[test]
fn the_recursion_limit_is_configurable() {
let sql = nested_parens(40);
let err = parse_with_options(&sql, TestDialect, limit(20))
.expect_err("40 nested parens exceed a limit of 20");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
parse_with_options(&sql, TestDialect, limit(200))
.expect("40 nested parens fit comfortably under a limit of 200");
}
#[test]
fn the_limit_is_configurable_through_the_engine_builder() {
let sql = nested_parens(40);
let tokens = tokenize(&sql).expect("the input lexes cleanly");
let mut parser = Parser::new(&sql, &tokens, TestDialect).with_recursion_limit(20);
let err = parser
.parse_next_statement()
.expect_err("the engine option bounds depth just like ParseOptions");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
}
#[test]
fn every_audited_recursion_site_rejects_past_the_limit() {
let deep = LOW_LIMIT + 8;
let vectors = [
("nested parentheses (parse_expr_bp)", nested_parens(deep)),
(
"nested parenthesized queries (parse_query)",
nested_paren_queries(deep),
),
(
"nested scalar subqueries (expr + query)",
nested_scalar_subqueries(deep),
),
(
"nested derived tables (parse_parenthesized_table_factor + query)",
nested_derived_tables(deep),
),
(
"nested join factors (parse_parenthesized_table_factor)",
nested_join_parens(deep),
),
(
"nested EXPLAIN statements (parse_statement)",
nested_explains(deep),
),
];
for (label, sql) in vectors {
let err = parse_with_options(&sql, TestDialect, limit(LOW_LIMIT))
.err()
.unwrap_or_else(|| panic!("{label}: expected a clean rejection, but the input parsed"));
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded, "{label}");
}
}
fn nested_trigger_body(n: usize) -> String {
format!(
"CREATE TRIGGER trg AFTER INSERT ON t BEGIN SELECT {}1{}; END",
"(SELECT ".repeat(n),
")".repeat(n),
)
}
#[test]
fn nested_trigger_body_past_the_limit_rejects_cleanly() {
use crate::dialect::Sqlite;
let sql = nested_trigger_body(LOW_LIMIT + 8);
let err = parse_with_options(&sql, Sqlite, limit(LOW_LIMIT))
.expect_err("a trigger body nested past the limit must be rejected, not crash");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
parse_with(
"CREATE TRIGGER trg AFTER INSERT ON t BEGIN SELECT 1; END",
Sqlite,
)
.expect("a shallow trigger body parses under the default limit");
}
#[test]
fn sibling_subtrees_do_not_accumulate_depth() {
let mut sql = String::from("SELECT ");
for i in 0..50 {
if i > 0 {
sql.push_str(", ");
}
sql.push_str("(1)");
}
parse_with_options(&sql, TestDialect, limit(8))
.expect("independent sibling groups must not accumulate recursion depth");
}
#[test]
fn frame_growth_trips_the_drift_sentinel_before_the_canary() {
let sentinel = std::thread::Builder::new()
.name("expr-frame-drift-sentinel".into())
.stack_size(1_600_000)
.spawn(|| {
parse_with(&nested_parens(64), TestDialect)
.expect("64 nested parens parse within the 1.6 MB sentinel stack");
parse_with(&nested_paren_queries(64), TestDialect)
.expect("64 nested subqueries parse within the 1.6 MB sentinel stack");
})
.expect("the sentinel thread spawns");
sentinel
.join()
.expect("the sentinel thread completes cleanly");
}
const COMPOUND_DIALECT: FeatureDialect = FeatureDialect {
features: &FeatureSet::MYSQL,
};
fn nested_compound(n: usize) -> String {
let mut sql = "BEGIN ".repeat(n);
sql.push_str("SELECT 1;");
sql.push_str(&" END;".repeat(n - 1));
sql.push_str(" END");
sql
}
fn parse_nested_compound(sql: &str, limit: usize) -> ParseResult<Statement<NoExt>> {
let tokens = tokenize(sql)?;
let mut parser = Parser::new(sql, &tokens, COMPOUND_DIALECT).with_recursion_limit(limit);
parser.parse_body_statement()
}
#[test]
fn nested_compound_body_past_the_limit_rejects_cleanly() {
let sql = nested_compound(LOW_LIMIT + 8);
let err = parse_nested_compound(&sql, LOW_LIMIT)
.expect_err("a compound block nested past the limit must be rejected, not crash");
assert_eq!(err.kind, ParseErrorKind::RecursionLimitExceeded);
parse_nested_compound(&nested_compound(4), 200).expect("a shallow block parses");
}
#[test]
fn compound_body_nesting_stays_within_the_frame_budget() {
let sentinel = std::thread::Builder::new()
.name("compound-frame-budget-sentinel".into())
.stack_size(1_600_000)
.spawn(|| {
parse_nested_compound(&nested_compound(64), 128)
.expect("64 nested compound blocks parse within the 1.6 MB sentinel stack");
})
.expect("the sentinel thread spawns");
sentinel
.join()
.expect("the sentinel thread completes cleanly");
}