use super::*;
use crate::storage::cas::CasStore;
use rusqlite::Connection;
use std::fs;
use std::path::PathBuf;
fn open_sqlite(dir: &std::path::Path, name: &str) -> (PathBuf, Connection) {
let path = dir.join(name);
let conn = Connection::open(&path).expect("open sqlite");
conn.pragma_update(None, "journal_mode", "WAL")
.expect("enable WAL");
(path, conn)
}
fn populate_db(conn: &Connection) {
conn.execute_batch(
"CREATE TABLE symbols (id INTEGER PRIMARY KEY, name TEXT, file TEXT);
CREATE INDEX idx_symbols_name ON symbols(name);
INSERT INTO symbols (name, file) VALUES ('foo', 'src/main.rs');
INSERT INTO symbols (name, file) VALUES ('bar', 'src/lib.rs');
INSERT INTO symbols (name, file) VALUES ('baz', 'src/lib.rs');",
)
.expect("populate");
}
fn populate_db_with_churn(conn: &Connection) {
conn.execute_batch(
"CREATE TABLE symbols (id INTEGER PRIMARY KEY, name TEXT, file TEXT);
CREATE INDEX idx_symbols_name ON symbols(name);
-- Bulk insert churn that fragments the B-tree and builds a freelist.
INSERT INTO symbols (name, file) VALUES ('churn1', 'x.rs');
INSERT INTO symbols (name, file) VALUES ('churn2', 'x.rs');
INSERT INTO symbols (name, file) VALUES ('churn3', 'x.rs');
INSERT INTO symbols (name, file) VALUES ('churn4', 'x.rs');
INSERT INTO symbols (name, file) VALUES ('churn5', 'x.rs');
DELETE FROM symbols WHERE file = 'x.rs';
-- Now insert the same final rows as the clean DB.
INSERT INTO symbols (name, file) VALUES ('foo', 'src/main.rs');
INSERT INTO symbols (name, file) VALUES ('bar', 'src/lib.rs');
INSERT INTO symbols (name, file) VALUES ('baz', 'src/lib.rs');",
)
.expect("populate with churn");
}
#[test]
fn test_db_to_cas_roundtrip_returns_hash() {
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let (_db_path, conn) = open_sqlite(dir.path(), "leindex.db");
populate_db(&conn);
let hash = db_to_cas_conn(&conn, &cas).expect("db_to_cas");
assert!(cas.exists(&hash), "blob must exist in CAS");
let bytes = cas.get(&hash).expect("get blob");
assert!(!bytes.is_empty(), "blob must not be empty");
assert_eq!(
&bytes[..16],
b"SQLite format 3\0",
"blob must start with SQLite magic"
);
}
#[test]
fn test_vacuum_dedup_identical_hash() {
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let (_path_a, conn_a) = open_sqlite(dir.path(), "a.db");
populate_db(&conn_a);
let hash_a = db_to_cas_conn(&conn_a, &cas).expect("db_to_cas a");
let (_path_b, conn_b) = open_sqlite(dir.path(), "b.db");
populate_db_with_churn(&conn_b);
let hash_b = db_to_cas_conn(&conn_b, &cas).expect("db_to_cas b");
assert_eq!(
hash_a, hash_b,
"VACUUM-normalised content hash must be identical for same logical content"
);
let count_a: i64 = conn_a
.query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
.unwrap();
let count_b: i64 = conn_b
.query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
.unwrap();
assert_eq!(count_a, count_b, "row counts must match for dedup test");
}
#[test]
fn test_noop_reindex_identical_hash() {
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let (_db_path, conn) = open_sqlite(dir.path(), "leindex.db");
populate_db(&conn);
let hash_1 = db_to_cas_conn(&conn, &cas).expect("first run");
let hash_2 = db_to_cas_conn(&conn, &cas).expect("second run");
assert_eq!(
hash_1, hash_2,
"no-op reindex must produce identical DB blob hash"
);
assert_eq!(
cas.blob_count().unwrap(),
1,
"CAS must not duplicate identical DB blobs"
);
}
#[test]
fn test_checkpoint_flushes_wal_before_vacuum() {
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let (db_path, conn) = open_sqlite(dir.path(), "leindex.db");
populate_db(&conn);
conn.execute(
"INSERT INTO symbols (name, file) VALUES ('post', 'y.rs')",
[],
)
.unwrap();
let _hash = db_to_cas_conn(&conn, &cas).expect("db_to_cas");
let wal_path = db_path.with_extension("db-wal");
if wal_path.exists() {
let meta = fs::metadata(&wal_path).expect("wal metadata");
assert_eq!(
meta.len(),
0,
"WAL must be truncated to 0 bytes after checkpoint(TRUNCATE)"
);
}
}
#[test]
fn test_vacuum_into_does_not_disturb_live_db() {
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let (_db_path, conn) = open_sqlite(dir.path(), "leindex.db");
populate_db(&conn);
let _hash = db_to_cas_conn(&conn, &cas).expect("db_to_cas");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
.expect("live query");
assert_eq!(count, 3, "live DB content must be intact after VACUUM INTO");
conn.execute(
"INSERT INTO symbols (name, file) VALUES ('after', 'z.rs')",
[],
)
.unwrap();
let new_count: i64 = conn
.query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
.unwrap();
assert_eq!(new_count, 4, "live DB still writable after VACUUM INTO");
}
#[test]
fn test_db_to_cas_via_storage_wrapper() {
use crate::storage::schema::Storage;
let dir = tempfile::tempdir().unwrap();
let cas_dir = dir.path().join("cas");
let cas = CasStore::open(&cas_dir).unwrap();
let db_path = dir.path().join("leindex.db");
let storage = Storage::open(&db_path).expect("open storage");
storage
.conn()
.execute_batch(
"CREATE TABLE t(x INTEGER);
INSERT INTO t VALUES (42);",
)
.unwrap();
let hash_via_storage = db_to_cas(&storage, &cas).expect("db_to_cas via storage");
let hash_via_conn = db_to_cas_conn(storage.conn(), &cas).expect("db_to_cas via conn");
assert_eq!(
hash_via_storage, hash_via_conn,
"Storage wrapper and Connection API must produce identical hashes"
);
}
#[test]
fn test_hash_determinism_across_cas_stores() {
let dir = tempfile::tempdir().unwrap();
let (_path_a, conn_a) = open_sqlite(dir.path(), "a.db");
populate_db(&conn_a);
let cas_a = CasStore::open(dir.path().join("cas_a")).unwrap();
let cas_b = CasStore::open(dir.path().join("cas_b")).unwrap();
let hash_a = db_to_cas_conn(&conn_a, &cas_a).expect("run on cas_a");
let hash_b = db_to_cas_conn(&conn_a, &cas_b).expect("run on cas_b");
assert_eq!(
hash_a, hash_b,
"hash must be deterministic across CAS instances"
);
}