use vibesql_executor::{InsertExecutor, SelectExecutor, TriggerExecutor};
use vibesql_parser::Parser;
use vibesql_storage::Database;
const NATIVE_TRIGGER_CAP: usize = 1000;
const DEEP_TEST_STACK: usize = 96 * 1024 * 1024;
#[cfg_attr(debug_assertions, allow(dead_code))]
const SERVER_WORKER_STACK_SIZE: usize = 8 * 1024 * 1024;
const SMALL_FIXED_STACK: usize = 2 * 1024 * 1024;
fn exec(db: &mut Database, sql: &str) -> Result<(), vibesql_executor::ExecutorError> {
match Parser::parse_sql(sql).expect("test SQL should parse") {
vibesql_ast::Statement::CreateTable(s) => {
vibesql_executor::CreateTableExecutor::execute(&s, db)?;
}
vibesql_ast::Statement::CreateTrigger(s) => {
TriggerExecutor::create_trigger_with_sql(db, &s, Some(sql))?;
}
vibesql_ast::Statement::Insert(s) => {
InsertExecutor::execute(db, &s)?;
}
other => panic!("unexpected statement in test: {other:?}"),
}
Ok(())
}
fn count_rows(db: &Database, table: &str) -> usize {
match Parser::parse_sql(&format!("SELECT * FROM {table}")).unwrap() {
vibesql_ast::Statement::Select(s) => {
SelectExecutor::new(db).execute(&s).expect("select should succeed").len()
}
_ => unreachable!(),
}
}
fn run_countdown(start: i64) -> (Database, Result<(), vibesql_executor::ExecutorError>) {
let mut db = Database::new();
exec(&mut db, "CREATE TABLE t2(a INTEGER PRIMARY KEY)").unwrap();
exec(
&mut db,
"CREATE TRIGGER t2_trig AFTER INSERT ON t2 WHEN (new.a > 0) BEGIN \
INSERT INTO t2 VALUES(new.a - 1); END",
)
.unwrap();
let res = exec(&mut db, &format!("INSERT INTO t2 VALUES({start})"));
(db, res)
}
fn on_big_stack<F: FnOnce() + Send + 'static>(name: &str, f: F) {
on_stack(name, DEEP_TEST_STACK, f);
}
fn on_stack<F: FnOnce() + Send + 'static>(name: &str, stack_size: usize, f: F) {
std::thread::Builder::new()
.name(name.to_string())
.stack_size(stack_size)
.spawn(f)
.expect("spawn deep-recursion test thread")
.join()
.unwrap_or_else(|_| panic!("{name}: trigger recursion overflowed the stack"));
}
#[test]
fn recursion_succeeds_past_old_cap_of_16() {
let (db, res) = run_countdown(20);
res.expect("depth-20 recursion should succeed under the cap");
assert_eq!(count_rows(&db, "t2"), 21, "rows 20..=0 should all be inserted");
}
#[test]
fn recursion_succeeds_at_500_levels() {
on_big_stack("trigger-depth-500", || {
let (db, res) = run_countdown(500);
res.expect("depth-500 recursion should succeed under the cap");
assert_eq!(count_rows(&db, "t2"), 501);
});
}
#[test]
fn recursion_succeeds_at_full_1000_cap() {
on_big_stack("trigger-depth-1000", || {
let (db, res) = run_countdown((NATIVE_TRIGGER_CAP - 1) as i64);
res.expect("recursion to the full 1000 cap should succeed");
assert_eq!(count_rows(&db, "t2"), NATIVE_TRIGGER_CAP);
});
}
#[test]
fn recursion_over_cap_errors_with_sqlite_wording_not_overflow() {
on_big_stack("trigger-over-cap", || {
let (db, res) = run_countdown(5000);
let err = res.expect_err("recursion exceeding the cap must error");
assert_eq!(
err.to_string(),
"too many levels of trigger recursion",
"over-cap recursion must use SQLite's exact wording",
);
assert_eq!(count_rows(&db, "t2"), 0, "failed recursive program leaves no rows");
});
}
#[cfg(not(debug_assertions))]
#[test]
fn ondemand_growth_reaches_1000_cap_on_small_stack_no_overflow() {
on_stack("trigger-grow-1000-small-stack", SMALL_FIXED_STACK, || {
let (db, res) = run_countdown((NATIVE_TRIGGER_CAP - 1) as i64);
res.expect("on-demand stack growth should let recursion reach the 1000 cap");
assert_eq!(count_rows(&db, "t2"), NATIVE_TRIGGER_CAP);
});
on_stack("trigger-grow-over-cap-small-stack", SMALL_FIXED_STACK, || {
let (db, res) = run_countdown(5000);
let err = res.expect_err("over-cap recursion must error cleanly, not overflow");
assert_eq!(
err.to_string(),
"too many levels of trigger recursion",
"over-cap recursion on a small stack must error with SQLite's wording",
);
assert_eq!(count_rows(&db, "t2"), 0);
});
}
#[cfg(not(debug_assertions))]
#[test]
fn server_runtime_stack_reaches_cap_cleanly_not_overflow() {
on_stack("trigger-server-stack-1000", SERVER_WORKER_STACK_SIZE, || {
let (db, res) = run_countdown(5000);
let err = res.expect_err("recursion exceeding the cap must error, not overflow");
assert_eq!(
err.to_string(),
"too many levels of trigger recursion",
"over-cap recursion on the server's 8 MiB stack must error cleanly",
);
assert_eq!(count_rows(&db, "t2"), 0, "failed recursive program leaves no rows");
});
}
#[cfg(debug_assertions)]
#[test]
fn ondemand_growth_clears_default_2mib_cliff_debug() {
on_stack("trigger-grow-debug", SMALL_FIXED_STACK, || {
let (db, res) = run_countdown(300);
res.expect("depth-300 must succeed on a 2 MiB stack via on-demand growth");
assert_eq!(count_rows(&db, "t2"), 301);
});
}