use codelore_lib::Options;
use codelore_lib::facts::FactsDb;
use codelore_lib::output::sqlite;
use codelore_lib::repo::GixRepo;
use duckdb::Connection;
fn schema_base_tables() -> Vec<String> {
const SCHEMA: &str = include_str!("../src/facts/schema_v1.sql");
SCHEMA
.lines()
.filter_map(|line| {
let rest = line.trim().strip_prefix("CREATE TABLE IF NOT EXISTS ")?;
let name = rest.split([' ', '(']).next()?;
(!name.is_empty()).then(|| name.to_string())
})
.collect()
}
#[test]
fn sqlite_full_dump_roundtrip() {
let tiny = codelore_lib::test_support::tiny_repo::build();
let repo = GixRepo::open(tiny.dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: tiny.dir.path().to_path_buf(),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("dump.db");
sqlite::write_full_fact_store_sqlite(&db, &opts, &path).expect("write");
assert!(path.exists(), "sqlite file should be created");
let reader = Connection::open_in_memory().expect("open");
let path_str = path.display().to_string();
reader
.execute_batch(&format!(
"INSTALL sqlite; LOAD sqlite; ATTACH '{path_str}' AS db (TYPE SQLITE);"
))
.expect("attach");
let commit_count: i64 = reader
.query_row("SELECT COUNT(*) FROM db.commits", [], |r| r.get(0))
.expect("count");
assert_eq!(commit_count, 5, "tiny_repo has 5 commits");
let expected = schema_base_tables();
assert!(
expected.len() >= 10,
"schema parse found too few base tables ({}): {expected:?}",
expected.len()
);
for table in &expected {
let sql = format!("SELECT COUNT(*) FROM db.{table}");
reader
.query_row::<i64, _, _>(&sql, [], |r| r.get(0))
.unwrap_or_else(|e| panic!("table {table} missing from sqlite dump: {e}"));
}
}
#[cfg(unix)]
#[test]
fn sqlite_extension_failure_explains_its_prerequisites() {
use std::os::unix::fs::PermissionsExt as _;
let repo = codelore_lib::test_support::tiny_repo::build();
let opts = Options {
repo_path: repo.dir.path().to_path_buf(),
..Options::default()
};
let gix = GixRepo::open(repo.dir.path()).expect("open fixture repo");
let db = FactsDb::open_or_ingest(&opts, &gix).expect("ingest fixture");
let home = repo.dir.path().join("locked-home");
std::fs::create_dir(&home).expect("create locked home");
std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o500))
.expect("make home unwritable");
let home_sql = home.display().to_string().replace('\'', "''");
db.execute_batch(&format!("SET home_directory='{home_sql}';"))
.expect("point DuckDB at the locked home");
let out = repo.dir.path().join("dump.sqlite");
let err = sqlite::write_full_fact_store_sqlite(&db, &opts, &out)
.expect_err("an unwritable extension home must fail the export");
let _ = std::fs::set_permissions(&home, std::fs::Permissions::from_mode(0o700));
let msg = err.to_string();
assert!(
msg.contains("hint:"),
"an extension-load failure must carry a hint, got: {msg}"
);
for expected in ["network access", "writable cache", ".duckdb/extensions"] {
assert!(
msg.contains(expected),
"the hint must name {expected:?} — the two prerequisites and where the \
cache lives are what make it actionable. Got: {msg}"
);
}
}