use vibesql_executor::{CreateIndexExecutor, CreateTableExecutor, InsertExecutor, SelectExecutor};
use vibesql_parser::Parser;
use vibesql_storage::{persistence::load::read_sql_dump, Database};
use vibesql_types::SqlValue;
fn exec_batch(db: &mut Database, sql: &str) {
for fragment in sql.split(';') {
let trimmed = strip_dump_comments(fragment);
if trimmed.is_empty() {
continue;
}
let stmt = Parser::parse_sql(&trimmed)
.unwrap_or_else(|e| panic!("Failed to parse `{trimmed}`: {e}"));
match stmt {
vibesql_ast::Statement::CreateTable(s) => {
CreateTableExecutor::execute(&s, db).expect("CREATE TABLE failed");
}
vibesql_ast::Statement::CreateIndex(s) => {
CreateIndexExecutor::execute(&s, db).expect("CREATE INDEX failed");
}
vibesql_ast::Statement::Insert(s) => {
InsertExecutor::execute(db, &s).expect("INSERT failed");
}
_ => {}
}
}
}
fn strip_dump_comments(fragment: &str) -> String {
fragment
.lines()
.filter(|line| {
let l = line.trim_start();
!l.is_empty() && !l.starts_with("--")
})
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string()
}
fn select_ints(db: &Database, sql: &str) -> Vec<i64> {
let stmt = Parser::parse_sql(sql).expect("Failed to parse SELECT");
let vibesql_ast::Statement::Select(select_stmt) = stmt else {
panic!("Expected SELECT");
};
let executor = SelectExecutor::new(db);
let rows = executor.execute(&select_stmt).expect("SELECT failed");
let mut out: Vec<i64> = rows
.iter()
.map(|row| match &row.values[0] {
SqlValue::Integer(i) => *i,
other => panic!("expected integer, got {other:?}"),
})
.collect();
out.sort_unstable();
out
}
fn save_and_reload(db: &Database, tag: &str) -> Database {
let path = std::env::temp_dir().join(format!(
"vibesql_5568_{tag}_{}_{}.sql",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
db.save_sql_dump(&path).expect("save_sql_dump failed");
let dump = read_sql_dump(&path).expect("read_sql_dump failed");
std::fs::remove_file(&path).ok();
let mut reloaded = Database::new();
exec_batch(&mut reloaded, &dump);
reloaded
}
#[test]
fn same_batch_index_persists_and_is_usable_after_reload() {
let mut db = Database::new();
exec_batch(
&mut db,
"CREATE TABLE t (a INTEGER, b INTEGER); \
CREATE INDEX ix ON t (b); \
INSERT INTO t VALUES (1, 10); \
INSERT INTO t VALUES (2, 20); \
INSERT INTO t VALUES (3, 20);",
);
assert!(
db.list_indexes().iter().any(|i| i.eq_ignore_ascii_case("ix")),
"index should be registered in the source database"
);
let reloaded = save_and_reload(&db, "same_batch");
assert!(
reloaded.list_indexes().iter().any(|i| i.eq_ignore_ascii_case("ix")),
"same-batch index was dropped from the dump; reloaded indexes: {:?}",
reloaded.list_indexes()
);
assert!(
reloaded
.list_indexes_for_table("t")
.iter()
.any(|i| i.eq_ignore_ascii_case("ix")),
"reloaded index not associated with table t"
);
assert_eq!(select_ints(&reloaded, "SELECT a FROM t WHERE b = 20"), vec![2, 3]);
assert_eq!(select_ints(&reloaded, "SELECT a FROM t WHERE b = 10"), vec![1]);
}
#[test]
fn same_batch_quoted_index_name_roundtrips() {
let mut db = Database::new();
exec_batch(
&mut db,
"CREATE TABLE t (c INTEGER, b INTEGER); \
CREATE INDEX \"x1 (b Asc, c Asc)\" ON t (b DESC, c); \
INSERT INTO t VALUES (7, 1); \
INSERT INTO t VALUES (8, 2);",
);
let reloaded = save_and_reload(&db, "quoted");
assert!(
reloaded
.list_indexes()
.iter()
.any(|i| i.eq_ignore_ascii_case("x1 (b asc, c asc)")),
"quoted same-batch index lost on reload; indexes: {:?}",
reloaded.list_indexes()
);
assert_eq!(select_ints(&reloaded, "SELECT c FROM t WHERE b = 2"), vec![8]);
}
#[test]
fn multiple_same_batch_indexes_all_persist() {
let mut db = Database::new();
exec_batch(
&mut db,
"CREATE TABLE t (a INTEGER, b INTEGER, c INTEGER); \
CREATE INDEX i_a ON t (a); \
CREATE INDEX i_bc ON t (b, c); \
INSERT INTO t VALUES (1, 5, 9); \
INSERT INTO t VALUES (2, 5, 8);",
);
let reloaded = save_and_reload(&db, "multi");
let names: Vec<String> = reloaded.list_indexes().iter().map(|s| s.to_lowercase()).collect();
assert!(names.iter().any(|n| n == "i_a"), "i_a missing: {names:?}");
assert!(names.iter().any(|n| n == "i_bc"), "i_bc missing: {names:?}");
assert_eq!(select_ints(&reloaded, "SELECT a FROM t WHERE b = 5 AND c = 8"), vec![2]);
assert_eq!(select_ints(&reloaded, "SELECT b FROM t WHERE a = 1"), vec![5]);
}
#[test]
fn separate_batch_index_still_persists() {
let mut db = Database::new();
exec_batch(
&mut db,
"CREATE TABLE t (a INTEGER, b INTEGER); \
INSERT INTO t VALUES (1, 100); \
INSERT INTO t VALUES (2, 200);",
);
exec_batch(&mut db, "CREATE INDEX ix_later ON t (b);");
let reloaded = save_and_reload(&db, "separate");
assert!(
reloaded
.list_indexes()
.iter()
.any(|i| i.eq_ignore_ascii_case("ix_later")),
"separately-created index lost on reload; indexes: {:?}",
reloaded.list_indexes()
);
assert_eq!(select_ints(&reloaded, "SELECT a FROM t WHERE b = 200"), vec![2]);
}