use super::*;
use std::fs;
#[test]
fn creates_schema_and_upserts_file() {
let db = IndexDb::open_memory().unwrap();
let id = db
.upsert_file("src/main.py", 1000, "abc123", 256, Some("py"))
.unwrap();
assert!(id > 0);
let file = db.get_file("src/main.py").unwrap().unwrap();
assert_eq!(file.content_hash, "abc123");
assert_eq!(file.size_bytes, 256);
let id2 = db
.upsert_file("src/main.py", 2000, "def456", 512, Some("py"))
.unwrap();
assert_eq!(id, id2);
let file = db.get_file("src/main.py").unwrap().unwrap();
assert_eq!(file.content_hash, "def456");
}
#[test]
fn fresh_file_check() {
let db = IndexDb::open_memory().unwrap();
db.upsert_file("a.py", 100, "hash1", 10, Some("py"))
.unwrap();
assert!(db.get_fresh_file("a.py", 100, "hash1").unwrap().is_some());
assert!(db.get_fresh_file("a.py", 200, "hash1").unwrap().is_none());
assert!(db.get_fresh_file("a.py", 100, "hash2").unwrap().is_none());
}
#[test]
fn inserts_and_queries_symbols() {
let db = IndexDb::open_memory().unwrap();
let file_id = db.upsert_file("main.py", 100, "h", 10, Some("py")).unwrap();
let syms = vec![
NewSymbol {
name: "Service",
kind: "class",
line: 1,
column_num: 1,
start_byte: 0,
end_byte: 50,
signature: "class Service:",
name_path: "Service",
parent_id: None,
},
NewSymbol {
name: "run",
kind: "method",
line: 2,
column_num: 5,
start_byte: 20,
end_byte: 48,
signature: "def run(self):",
name_path: "Service/run",
parent_id: None,
},
];
let ids = db.insert_symbols(file_id, &syms).unwrap();
assert_eq!(ids.len(), 2);
let found = db.find_symbols_by_name("Service", None, true, 10).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].kind, "class");
let found = db
.find_symbols_by_name("run", Some("main.py"), true, 10)
.unwrap();
assert_eq!(found.len(), 1);
let found = db.find_symbols_by_name("erv", None, false, 10).unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "Service");
}
#[test]
fn upsert_file_clears_old_symbols() {
let db = IndexDb::open_memory().unwrap();
let file_id = db.upsert_file("a.py", 100, "h1", 10, Some("py")).unwrap();
db.insert_symbols(
file_id,
&[NewSymbol {
name: "Old",
kind: "class",
line: 1,
column_num: 1,
start_byte: 0,
end_byte: 10,
signature: "class Old:",
name_path: "Old",
parent_id: None,
}],
)
.unwrap();
let file_id2 = db.upsert_file("a.py", 200, "h2", 20, Some("py")).unwrap();
assert_eq!(file_id, file_id2);
let found = db.find_symbols_by_name("Old", None, true, 10).unwrap();
assert!(found.is_empty());
}
#[test]
fn streams_symbols_grouped_by_file_in_path_order() {
let db = IndexDb::open_memory().unwrap();
let b_file_id = db.upsert_file("b.py", 100, "hb", 10, Some("py")).unwrap();
let a_file_id = db.upsert_file("a.py", 100, "ha", 10, Some("py")).unwrap();
db.insert_symbols(
b_file_id,
&[
NewSymbol {
name: "b_second",
kind: "function",
line: 2,
column_num: 0,
start_byte: 20,
end_byte: 30,
signature: "def b_second():",
name_path: "b_second",
parent_id: None,
},
NewSymbol {
name: "b_first",
kind: "function",
line: 1,
column_num: 0,
start_byte: 0,
end_byte: 10,
signature: "def b_first():",
name_path: "b_first",
parent_id: None,
},
],
)
.unwrap();
db.insert_symbols(
a_file_id,
&[NewSymbol {
name: "a_only",
kind: "function",
line: 1,
column_num: 0,
start_byte: 0,
end_byte: 10,
signature: "def a_only():",
name_path: "a_only",
parent_id: None,
}],
)
.unwrap();
let mut groups = Vec::new();
let count = db
.for_each_file_symbols_with_bytes(|file_path, symbols| {
groups.push((
file_path,
symbols
.into_iter()
.map(|symbol| symbol.name)
.collect::<Vec<_>>(),
));
Ok(())
})
.unwrap();
assert_eq!(count, 3);
assert_eq!(
groups,
vec![
("a.py".to_string(), vec!["a_only".to_string()]),
(
"b.py".to_string(),
vec!["b_first".to_string(), "b_second".to_string()]
),
]
);
}
#[test]
fn import_graph_operations() {
let db = IndexDb::open_memory().unwrap();
let main_id = db
.upsert_file("main.py", 100, "h1", 10, Some("py"))
.unwrap();
let utils_id = db
.upsert_file("utils.py", 100, "h2", 10, Some("py"))
.unwrap();
let _models_id = db
.upsert_file("models.py", 100, "h3", 10, Some("py"))
.unwrap();
db.insert_imports(
main_id,
&[NewImport {
target_path: "utils.py".into(),
raw_import: "utils".into(),
}],
)
.unwrap();
db.insert_imports(
utils_id,
&[NewImport {
target_path: "models.py".into(),
raw_import: "models".into(),
}],
)
.unwrap();
let importers = db.get_importers("utils.py").unwrap();
assert_eq!(importers, vec!["main.py"]);
let imports_of = db.get_imports_of("main.py").unwrap();
assert_eq!(imports_of, vec!["utils.py"]);
let graph = db.build_import_graph().unwrap();
assert_eq!(graph.len(), 3);
assert_eq!(graph["utils.py"].1, vec!["main.py"]); }
#[test]
fn content_hash_is_deterministic() {
let h1 = content_hash(b"hello world");
let h2 = content_hash(b"hello world");
let h3 = content_hash(b"hello world!");
assert_eq!(h1, h2);
assert_ne!(h1, h3);
}
#[test]
fn with_transaction_auto_rollback_on_error() {
let mut db = IndexDb::open_memory().unwrap();
let result: anyhow::Result<()> = db.with_transaction(|conn| {
ops::upsert_file(conn, "rollback_test.py", 100, "h1", 10, Some("py"))?;
anyhow::bail!("simulated error");
});
assert!(result.is_err());
assert!(db.get_file("rollback_test.py").unwrap().is_none());
}
#[test]
fn open_recreates_corrupt_db_and_wal_sidecars() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("symbols.db");
let wal_path = dir.path().join("symbols.db-wal");
let shm_path = dir.path().join("symbols.db-shm");
fs::write(&db_path, b"not a sqlite database").unwrap();
fs::write(&wal_path, b"bad wal").unwrap();
fs::write(&shm_path, b"bad shm").unwrap();
let db = IndexDb::open(&db_path).unwrap();
assert_eq!(db.file_count().unwrap(), 0);
let file_id = db
.upsert_file("src/lib.rs", 100, "hash", 12, Some("rs"))
.unwrap();
assert!(file_id > 0);
assert!(db.get_file("src/lib.rs").unwrap().is_some());
assert!(db_path.is_file());
let backup_names: Vec<String> = fs::read_dir(dir.path())
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| name.contains(".corrupt-"))
.collect();
assert!(
backup_names
.iter()
.any(|name| name.starts_with("symbols.db.corrupt-")),
"expected quarantined primary db file, found {backup_names:?}"
);
}
#[test]
fn quarantine_corrupt_sqlite_files_moves_sidecars_when_present() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("symbols.db");
let wal_path = dir.path().join("symbols.db-wal");
let shm_path = dir.path().join("symbols.db-shm");
fs::write(&db_path, b"not a sqlite database").unwrap();
fs::write(&wal_path, b"bad wal").unwrap();
fs::write(&shm_path, b"bad shm").unwrap();
let backups = quarantine_corrupt_sqlite_files(&db_path).unwrap();
let backup_names: Vec<String> = backups
.iter()
.map(|path| path.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert!(
backup_names
.iter()
.any(|name| name.starts_with("symbols.db.corrupt-")),
"expected quarantined primary db file, found {backup_names:?}"
);
assert!(
backup_names
.iter()
.any(|name| name.starts_with("symbols.db-wal.corrupt-")),
"expected quarantined wal sidecar, found {backup_names:?}"
);
assert!(
backup_names
.iter()
.any(|name| name.starts_with("symbols.db-shm.corrupt-")),
"expected quarantined shm sidecar, found {backup_names:?}"
);
}