use vibesql_executor::{
CreateIndexExecutor, CreateTableExecutor, DropIndexExecutor, DropTableExecutor, SelectExecutor,
TriggerExecutor, ViewExecutor,
};
use vibesql_parser::Parser;
use vibesql_storage::{Database, Row};
use vibesql_types::SqlValue;
fn exec_create_table(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse CREATE TABLE");
match stmt {
vibesql_ast::Statement::CreateTable(s) => {
CreateTableExecutor::execute(&s, db).expect("CREATE TABLE")
}
other => panic!("expected CREATE TABLE, got {other:?}"),
};
}
fn exec_create_index(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse CREATE INDEX");
match stmt {
vibesql_ast::Statement::CreateIndex(s) => {
CreateIndexExecutor::execute(&s, db).expect("CREATE INDEX")
}
other => panic!("expected CREATE INDEX, got {other:?}"),
};
}
fn exec_drop_index(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse DROP INDEX");
match stmt {
vibesql_ast::Statement::DropIndex(s) => {
DropIndexExecutor::execute(&s, db).expect("DROP INDEX")
}
other => panic!("expected DROP INDEX, got {other:?}"),
};
}
fn exec_insert(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse INSERT");
match stmt {
vibesql_ast::Statement::Insert(s) => {
vibesql_executor::InsertExecutor::execute(db, &s).expect("INSERT");
}
other => panic!("expected INSERT, got {other:?}"),
}
}
fn exec_drop_table(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse DROP TABLE");
match stmt {
vibesql_ast::Statement::DropTable(s) => {
DropTableExecutor::execute(&s, db).expect("DROP TABLE")
}
other => panic!("expected DROP TABLE, got {other:?}"),
};
}
fn exec_create_trigger(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse CREATE TRIGGER");
match stmt {
vibesql_ast::Statement::CreateTrigger(s) => {
TriggerExecutor::create_trigger(db, &s).expect("CREATE TRIGGER");
}
other => panic!("expected CREATE TRIGGER, got {other:?}"),
};
}
fn exec_create_view(db: &mut Database, sql: &str) {
let stmt = Parser::parse_sql(sql).expect("parse CREATE VIEW");
match stmt {
vibesql_ast::Statement::CreateView(s) => {
ViewExecutor::execute_create_view(&s, db).expect("CREATE VIEW");
}
other => panic!("expected CREATE VIEW, got {other:?}"),
};
}
fn select_is_err(db: &Database, sql: &str) -> bool {
let stmt = match Parser::parse_sql(sql) {
Ok(s) => s,
Err(_) => return true,
};
match stmt {
vibesql_ast::Statement::Select(s) => {
SelectExecutor::new(db).execute_with_columns(&s).is_err()
}
other => panic!("expected SELECT, got {other:?}"),
}
}
fn select(db: &Database, sql: &str) -> (Vec<String>, Vec<Row>) {
let stmt = Parser::parse_sql(sql).expect("parse SELECT");
match stmt {
vibesql_ast::Statement::Select(s) => {
let executor = SelectExecutor::new(db);
let r = executor.execute_with_columns(&s).expect("SELECT");
(r.columns, r.rows)
}
other => panic!("expected SELECT, got {other:?}"),
}
}
fn names(rows: &[Row]) -> Vec<String> {
rows.iter()
.map(|r| match &r.values[0] {
SqlValue::Varchar(s) => s.to_string(),
other => panic!("expected text name, got {other:?}"),
})
.collect()
}
#[test]
fn temp_table_and_index_listed_in_temp_master() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TEMP TABLE t (a)");
exec_create_index(&mut db, "CREATE INDEX i ON t(a)");
let (_, rows) = select(&db, "SELECT name FROM sqlite_temp_master ORDER BY name");
let listed = names(&rows);
assert!(listed.contains(&"t".to_string()), "temp table t should be in sqlite_temp_master: {listed:?}");
assert!(listed.contains(&"i".to_string()), "temp index i should be in sqlite_temp_master: {listed:?}");
}
#[test]
fn temp_index_absent_from_sqlite_master() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TEMP TABLE t (a)");
exec_create_index(&mut db, "CREATE INDEX i ON t(a)");
let (_, rows) = select(&db, "SELECT name FROM sqlite_master");
let listed = names(&rows);
assert!(!listed.contains(&"t".to_string()), "temp table must NOT be in sqlite_master: {listed:?}");
assert!(!listed.contains(&"i".to_string()), "temp index must NOT be in sqlite_master: {listed:?}");
}
#[test]
fn main_index_in_master_not_temp_master() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TABLE base (a)");
exec_create_index(&mut db, "CREATE INDEX mi ON base(a)");
let (_, master) = select(&db, "SELECT name FROM sqlite_master");
assert!(names(&master).contains(&"mi".to_string()), "main index should be in sqlite_master");
let (_, temp) = select(&db, "SELECT name FROM sqlite_temp_master");
assert!(!names(&temp).contains(&"mi".to_string()), "main index must NOT be in sqlite_temp_master");
}
fn scalar_i64(db: &Database, sql: &str) -> i64 {
let (_, rows) = select(db, sql);
assert_eq!(rows.len(), 1, "expected one result row for {sql}");
match &rows[0].values[0] {
SqlValue::Integer(n) => *n,
SqlValue::Bigint(n) => *n,
other => panic!("expected integer scalar, got {other:?}"),
}
}
#[test]
fn sql_level_same_name_index_across_schemas_coexist() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TABLE main_t (a)");
exec_create_table(&mut db, "CREATE TEMP TABLE temp_t (a)");
exec_insert(&mut db, "INSERT INTO main_t VALUES (1)");
exec_insert(&mut db, "INSERT INTO main_t VALUES (2)");
exec_insert(&mut db, "INSERT INTO temp.temp_t VALUES (10)");
exec_create_index(&mut db, "CREATE INDEX ix ON main_t(a)");
exec_create_index(&mut db, "CREATE INDEX ix ON temp_t(a)");
let (_, master) = select(&db, "SELECT name FROM sqlite_master WHERE type='index'");
assert!(names(&master).contains(&"ix".to_string()), "main.ix must be in sqlite_master");
let (_, temp_master) =
select(&db, "SELECT name FROM sqlite_temp_master WHERE type='index'");
assert!(
names(&temp_master).contains(&"ix".to_string()),
"temp.ix must be in sqlite_temp_master"
);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM main_t WHERE a = 1"), 1);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM main_t WHERE a = 2"), 1);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM temp.temp_t WHERE a = 10"), 1);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM temp.temp_t WHERE a = 1"), 0);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM main_t WHERE a = 10"), 0);
exec_drop_index(&mut db, "DROP INDEX ix");
let (_, temp_after) =
select(&db, "SELECT name FROM sqlite_temp_master WHERE type='index'");
assert!(
!names(&temp_after).contains(&"ix".to_string()),
"unqualified DROP INDEX must remove the temp index first"
);
let (_, master_after) = select(&db, "SELECT name FROM sqlite_master WHERE type='index'");
assert!(
names(&master_after).contains(&"ix".to_string()),
"main.ix must survive an unqualified DROP INDEX that resolved to temp.ix"
);
assert_eq!(scalar_i64(&db, "SELECT count(*) FROM main_t WHERE a = 2"), 1);
exec_drop_index(&mut db, "DROP INDEX ix");
let (_, master_final) = select(&db, "SELECT name FROM sqlite_master WHERE type='index'");
assert!(
!names(&master_final).contains(&"ix".to_string()),
"the second DROP INDEX removes the remaining main index"
);
}
#[test]
fn temp_index_dropped_with_temp_table() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TABLE base (a)");
exec_create_table(&mut db, "CREATE TEMP TABLE t (a)");
exec_create_index(&mut db, "CREATE INDEX mi ON base(a)"); exec_create_index(&mut db, "CREATE INDEX ti ON t(a)");
let (_, before) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='index'");
assert!(names(&before).contains(&"ti".to_string()));
exec_drop_table(&mut db, "DROP TABLE temp.t");
let (_, after_temp) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='index'");
assert!(
!names(&after_temp).contains(&"ti".to_string()),
"temp index should be dropped with the temp table"
);
let (_, after_main) = select(&db, "SELECT name FROM sqlite_master WHERE type='index'");
assert!(
names(&after_main).contains(&"mi".to_string()),
"main index must survive dropping the temp table"
);
}
#[test]
fn temp_trigger_dropped_with_temp_table_view_remains() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TEMP TABLE t (a)");
exec_create_trigger(&mut db, "CREATE TEMP TRIGGER tr AFTER INSERT ON t BEGIN SELECT 1; END");
exec_create_view(&mut db, "CREATE TEMP VIEW v AS SELECT * FROM t");
let (_, before) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='trigger'");
assert!(names(&before).contains(&"tr".to_string()), "temp trigger should be listed before drop");
let (_, before_v) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='view'");
assert!(names(&before_v).contains(&"v".to_string()), "temp view should be listed before drop");
exec_drop_table(&mut db, "DROP TABLE temp.t");
let (_, after_tr) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='trigger'");
assert!(
!names(&after_tr).contains(&"tr".to_string()),
"temp trigger ON the dropped table must be cascade-dropped"
);
let (_, after_v) = select(&db, "SELECT name FROM sqlite_temp_master WHERE type='view'");
assert!(
names(&after_v).contains(&"v".to_string()),
"a view merely referencing the dropped table must NOT be auto-dropped"
);
assert!(
select_is_err(&db, "SELECT * FROM v"),
"the surviving view must error on use after its base table is dropped"
);
}
#[test]
fn main_drop_table_drops_only_triggers_on_that_table() {
let mut db = Database::new();
exec_create_table(&mut db, "CREATE TABLE t (a)");
exec_create_table(&mut db, "CREATE TABLE other (b)");
exec_create_trigger(&mut db, "CREATE TRIGGER tr AFTER INSERT ON t BEGIN SELECT 1; END");
exec_create_trigger(
&mut db,
"CREATE TRIGGER tr2 AFTER INSERT ON other BEGIN INSERT INTO t VALUES (1); END",
);
exec_drop_table(&mut db, "DROP TABLE t");
let (_, after) = select(&db, "SELECT name FROM sqlite_master WHERE type='trigger'");
let listed = names(&after);
assert!(
!listed.contains(&"tr".to_string()),
"trigger ON the dropped table must be removed: {listed:?}"
);
assert!(
listed.contains(&"tr2".to_string()),
"trigger ON a different table (even if it references the dropped one) must survive: {listed:?}"
);
}
#[test]
fn temp_trigger_and_view_are_connection_local() {
let mut db_a = Database::new();
exec_create_table(&mut db_a, "CREATE TEMP TABLE t (a)");
exec_create_trigger(&mut db_a, "CREATE TEMP TRIGGER tr AFTER INSERT ON t BEGIN SELECT 1; END");
exec_create_view(&mut db_a, "CREATE TEMP VIEW v AS SELECT * FROM t");
let (_, a_objs) = select(&db_a, "SELECT name FROM sqlite_temp_master");
let a_listed = names(&a_objs);
assert!(a_listed.contains(&"tr".to_string()));
assert!(a_listed.contains(&"v".to_string()));
let db_b = Database::new();
let (_, b_objs) = select(&db_b, "SELECT name FROM sqlite_temp_master");
let b_listed = names(&b_objs);
assert!(
!b_listed.contains(&"tr".to_string()),
"connection B must not see connection A's temp trigger: {b_listed:?}"
);
assert!(
!b_listed.contains(&"v".to_string()),
"connection B must not see connection A's temp view: {b_listed:?}"
);
drop(db_a);
}