use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SqlxCacheCheck {
Missing,
Empty,
Ready(usize),
}
pub(super) fn sqlx_cache_staleness(sqlx_dir: &Path, src_dir: &Path) -> Result<Option<String>> {
if !sqlx_dir.exists() {
return Ok(Some(".sqlx/ missing".to_string()));
}
let entries: Vec<_> = match std::fs::read_dir(sqlx_dir) {
Ok(it) => it.flatten().collect(),
Err(e) => return Ok(Some(format!(".sqlx/ unreadable: {e}"))),
};
if entries.is_empty() {
return Ok(Some(".sqlx/ empty".to_string()));
}
let cache_oldest = entries
.iter()
.filter(|e| e.file_name().to_string_lossy().starts_with("query-"))
.filter_map(|e| e.metadata().ok())
.filter_map(|m| m.modified().ok())
.min();
if cache_oldest.is_none() {
return Ok(Some(".sqlx/ has no query entries".to_string()));
}
let mut newest_src: Option<std::time::SystemTime> = None;
let mut stack = vec![src_dir.to_path_buf()];
while let Some(path) = stack.pop() {
let Ok(meta) = std::fs::metadata(&path) else {
continue;
};
if meta.is_dir() {
if let Ok(it) = std::fs::read_dir(&path) {
for e in it.flatten() {
stack.push(e.path());
}
}
} else if path.extension().and_then(|s| s.to_str()) == Some("rs")
&& let Ok(modified) = meta.modified()
{
newest_src = Some(newest_src.map(|n| n.max(modified)).unwrap_or(modified));
}
}
if let (Some(src), Some(cache)) = (newest_src, cache_oldest)
&& src > cache
{
return Ok(Some("Rust source newer than .sqlx/".to_string()));
}
Ok(None)
}
pub(super) fn resolve_database_url(config_path: &str) -> Result<String> {
if let Ok(url) = std::env::var("DATABASE_URL")
&& !url.is_empty()
{
return Ok(url);
}
let path = Path::new(config_path);
if !path.exists() {
anyhow::bail!("DATABASE_URL not set and {} not found", config_path);
}
let cfg = forge_core::config::ForgeConfig::from_file(config_path)
.map_err(|e| anyhow::anyhow!("failed to load {config_path}: {e}"))?;
Ok(cfg.database.url().to_string())
}
pub(super) fn project_uses_compile_time_sqlx_macros(src_dir: &Path) -> Result<bool> {
if !src_dir.exists() {
return Ok(false);
}
for entry in std::fs::read_dir(src_dir)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
if file_type.is_dir() {
if project_uses_compile_time_sqlx_macros(&path)? {
return Ok(true);
}
continue;
}
if !file_type.is_file() || path.extension().is_none_or(|ext| ext != "rs") {
continue;
}
let content = std::fs::read_to_string(&path)?;
if file_uses_sqlx_macros(&content) {
return Ok(true);
}
}
Ok(false)
}
pub(super) fn file_uses_sqlx_macros(content: &str) -> bool {
const NEEDLES: &[&str] = &[
"sqlx::query!(",
"sqlx::query_as!(",
"sqlx::query_scalar!(",
"sqlx::query_file!(",
"sqlx::query_file_as!(",
];
content.lines().any(|line| {
let code = match line.split_once("//") {
Some((before, _)) => before,
None => line,
};
NEEDLES.iter().any(|needle| code.contains(needle))
})
}
pub(super) fn inspect_sqlx_cache(sqlx_dir: &Path) -> Result<SqlxCacheCheck> {
if !sqlx_dir.exists() {
return Ok(SqlxCacheCheck::Missing);
}
let query_file_count = std::fs::read_dir(sqlx_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with("query-"))
.count();
if query_file_count == 0 {
Ok(SqlxCacheCheck::Empty)
} else {
Ok(SqlxCacheCheck::Ready(query_file_count))
}
}