use std::path::Path;
use std::sync::Arc;
use tokio::sync::{Mutex, MutexGuard};
use crate::migration::{
apply_indexes, check_schema_version, record_schema_version, upgrade_legacy_timestamps,
};
use crate::models::{
ArchitecturalDecision, CodeGraphEdge, DependencyPackage, Project, ProjectDependency,
SchemaVersion, Scope, SourceFile, Symbol, SymbolReference, ToolInvocationMetric,
};
#[derive(Clone, Debug)]
pub struct ScryerDb {
inner: Arc<Mutex<toasty::Db>>,
}
impl ScryerDb {
pub async fn new_in_memory() -> anyhow::Result<Self> {
Self::connect("turso::memory:").await
}
pub async fn connect(url: &str) -> anyhow::Result<Self> {
let mut builder = toasty::Db::builder();
builder.models(toasty::models![
Project,
SourceFile,
Scope,
Symbol,
SymbolReference,
CodeGraphEdge,
ArchitecturalDecision,
ToolInvocationMetric,
DependencyPackage,
ProjectDependency,
SchemaVersion,
]);
let mut db = builder.connect(url).await?;
init_schema_and_indexes(&mut db).await?;
Ok(Self {
inner: Arc::new(Mutex::new(db)),
})
}
pub async fn connect_file(path: &Path) -> anyhow::Result<Self> {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
let driver = toasty_driver_turso::Turso::file(path);
let mut builder = toasty::Db::builder();
builder.models(toasty::models![
Project,
SourceFile,
Scope,
Symbol,
SymbolReference,
CodeGraphEdge,
ArchitecturalDecision,
ToolInvocationMetric,
DependencyPackage,
ProjectDependency,
SchemaVersion,
]);
let mut db = builder
.build(driver)
.await
.map_err(|e| explain_open_error(path, e.into()))?;
init_schema_and_indexes(&mut db).await?;
Ok(Self {
inner: Arc::new(Mutex::new(db)),
})
}
pub async fn connect_with_auth(
url_or_path: &str,
auth_token: Option<&str>,
) -> anyhow::Result<Self> {
if url_or_path == "turso::memory:" || url_or_path == ":memory:" {
return Self::new_in_memory().await;
}
if url_or_path.starts_with("turso:")
|| url_or_path.starts_with("libsql:")
|| url_or_path.starts_with("https:")
|| url_or_path.starts_with("http:")
{
let url = if let Some(token) = auth_token {
if url_or_path.contains('?') {
format!("{url_or_path}&authToken={token}")
} else {
format!("{url_or_path}?authToken={token}")
}
} else {
url_or_path.to_string()
};
return Self::connect(&url).await;
}
let path = Path::new(url_or_path);
Self::connect_file(path).await
}
pub async fn lock(&self) -> MutexGuard<'_, toasty::Db> {
self.inner.lock().await
}
pub fn inner(&self) -> Arc<Mutex<toasty::Db>> {
Arc::clone(&self.inner)
}
}
async fn init_schema_and_indexes(db: &mut toasty::Db) -> anyhow::Result<()> {
let existing_tables =
toasty::sql::query("SELECT name FROM sqlite_master WHERE type='table' AND name='project'")
.exec(db)
.await?;
if existing_tables.is_empty() {
db.push_schema().await?;
}
check_schema_version(db).await?;
apply_indexes(db).await?;
record_schema_version(db).await?;
upgrade_legacy_timestamps(db).await?;
Ok(())
}
fn explain_open_error(path: &Path, err: anyhow::Error) -> anyhow::Error {
let text = err.root_cause().to_string();
if !(text.contains("Failed locking file") || text.contains("locked by another process")) {
return err;
}
anyhow::anyhow!(
"Scryer database {} is in use by another Scryer process.\n\
A local Scryer database can only be opened by one process. `scryer serve` shares it \
through a background daemon, so:\n \
- run `scryer daemon status` to see the daemon that owns it, or `scryer daemon stop` to stop it\n \
- drop `--no-daemon` so `scryer serve` connects to that daemon\n \
- or point this command at another database with `--db-url` / `SCRYER_DB_URL`\n\
(Turso: {text})",
path.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_errors_explain_how_to_share_the_database() {
let err = anyhow::anyhow!(
"Locking error: Failed locking file '/tmp/scryer.db'. File is locked by another process"
);
let msg = explain_open_error(Path::new("/tmp/scryer.db"), err).to_string();
assert!(msg.contains("in use by another Scryer process"));
assert!(msg.contains("scryer daemon status"));
assert!(msg.contains("Failed locking file"));
}
#[test]
fn other_errors_pass_through() {
let err = anyhow::anyhow!("disk full");
let msg = explain_open_error(Path::new("/tmp/scryer.db"), err).to_string();
assert_eq!(msg, "disk full");
}
}