use crate::errors::AppError;
use crate::paths::AppPaths;
use crate::pragmas::{apply_connection_pragmas, apply_init_pragmas, ensure_wal_mode};
use rusqlite::Connection;
use sqlite_vec::sqlite3_vec_init;
use std::path::Path;
use std::sync::OnceLock;
static VEC_EXTENSION_REGISTERED: OnceLock<()> = OnceLock::new();
pub fn register_vec_extension() {
VEC_EXTENSION_REGISTERED.get_or_init(|| {
#[allow(clippy::missing_transmute_annotations)]
unsafe {
rusqlite::ffi::sqlite3_auto_extension(Some(std::mem::transmute(
sqlite3_vec_init as *const (),
)));
}
});
}
pub fn open_rw(path: &Path) -> Result<Connection, AppError> {
let conn = Connection::open(path)?;
apply_connection_pragmas(&conn)?;
apply_secure_permissions(path);
Ok(conn)
}
pub fn ensure_schema(conn: &mut Connection) -> Result<(), AppError> {
crate::migrations::runner()
.run(conn)
.map_err(|e| AppError::Internal(anyhow::anyhow!("migration failed: {e}")))?;
conn.execute_batch(&format!(
"PRAGMA user_version = {};",
crate::constants::SCHEMA_USER_VERSION
))?;
Ok(())
}
pub fn ensure_db_ready(paths: &AppPaths) -> Result<(), AppError> {
register_vec_extension();
paths.ensure_dirs()?;
let db_existed = paths.db.exists();
if !db_existed {
tracing::info!(
path = %paths.db.display(),
schema_version = crate::constants::CURRENT_SCHEMA_VERSION,
"creating database (auto-init)"
);
}
let mut conn = open_rw(&paths.db)?;
if !db_existed {
apply_init_pragmas(&conn)?;
}
let current_user_version: i64 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap_or(0);
let target_user_version = crate::constants::SCHEMA_USER_VERSION;
if current_user_version < target_user_version {
if db_existed {
tracing::warn!(
from = current_user_version,
to = target_user_version,
path = %paths.db.display(),
"auto-migrating database schema"
);
}
crate::migrations::runner()
.run(&mut conn)
.map_err(|e| AppError::Internal(anyhow::anyhow!("auto-migration failed: {e}")))?;
conn.execute_batch(&format!("PRAGMA user_version = {target_user_version};"))?;
if !db_existed {
insert_default_schema_meta(&conn)?;
}
ensure_wal_mode(&conn)?;
}
Ok(())
}
fn insert_default_schema_meta(conn: &Connection) -> Result<(), AppError> {
conn.execute(
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
rusqlite::params![crate::constants::CURRENT_SCHEMA_VERSION.to_string()],
)?;
conn.execute(
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', 'multilingual-e5-small')",
[],
)?;
conn.execute(
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', '384')",
[],
)?;
conn.execute(
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
[],
)?;
conn.execute(
"INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
)?;
Ok(())
}
#[allow(unused_variables)]
fn apply_secure_permissions(path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let candidates = [
path.to_path_buf(),
path.with_extension(format!(
"{}-wal",
path.extension()
.and_then(|e| e.to_str())
.unwrap_or("sqlite")
)),
path.with_extension(format!(
"{}-shm",
path.extension()
.and_then(|e| e.to_str())
.unwrap_or("sqlite")
)),
];
for file in candidates.iter() {
if file.exists() {
if let Ok(meta) = std::fs::metadata(file) {
let mut perms = meta.permissions();
perms.set_mode(0o600);
let _ = std::fs::set_permissions(file, perms);
}
}
}
}
}
pub fn open_ro(path: &Path) -> Result<Connection, AppError> {
let conn = Connection::open_with_flags(
path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
)?;
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
Ok(conn)
}