Skip to content

Commit 159ca3c

Browse files
committed
tasks: implement execIfModified
1 parent 0de6c6a commit 159ca3c

File tree

20 files changed

+2220
-169
lines changed

20 files changed

+2220
-169
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"http-client-tls",
1010
"nix-conf-parser",
1111
"xtask",
12+
"devenv-cache-core",
1213
]
1314

1415
[workspace.package]
@@ -23,6 +24,7 @@ devenv = { path = "devenv" }
2324
devenv-eval-cache = { path = "devenv-eval-cache" }
2425
devenv-run-tests = { path = "devenv-run-tests" }
2526
devenv-tasks = { path = "devenv-tasks" }
27+
devenv-cache-core = { path = "devenv-cache-core" }
2628
http-client-tls = { path = "http-client-tls" }
2729
nix-conf-parser = { path = "nix-conf-parser" }
2830
xtask = { path = "xtask" }
@@ -83,6 +85,7 @@ which = "7.0.2"
8385
whoami = "1.5.1"
8486
xdg = "2.5.2"
8587
tokio-tar = "0.3.1"
88+
walkdir = "2.3"
8689

8790
# The version of rustls must match the version used by reqwest to set up rustls-platform-verifier.
8891
# If you encounter an error, lock these versions down.

devenv-cache-core/Cargo.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[package]
2+
name = "devenv-cache-core"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "Core utilities for file tracking and caching in devenv"
6+
license = "MIT"
7+
8+
[dependencies]
9+
# Database
10+
sqlx = { workspace = true, features = [
11+
"runtime-tokio",
12+
"tls-rustls",
13+
"sqlite",
14+
"migrate",
15+
"macros",
16+
] }
17+
18+
# Error handling
19+
thiserror.workspace = true
20+
miette.workspace = true
21+
eyre.workspace = true
22+
23+
# Hashing
24+
blake3.workspace = true
25+
26+
# File operations
27+
walkdir.workspace = true
28+
29+
# Async runtime
30+
tokio = { workspace = true, features = ["fs", "macros", "time"] }
31+
32+
# Logging
33+
tracing.workspace = true
34+
serde_json.workspace = true
35+
36+
[dev-dependencies]
37+
tempfile.workspace = true
38+
tokio = { workspace = true, features = ["rt", "macros"] }

devenv-cache-core/src/db.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use crate::error::{CacheError, CacheResult};
2+
use sqlx::migrate::MigrateDatabase;
3+
use sqlx::sqlite::{
4+
SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteSynchronous,
5+
};
6+
use std::path::PathBuf;
7+
use std::str::FromStr;
8+
use std::time::Duration;
9+
use tracing::{debug, error, info};
10+
11+
/// Database connection manager
12+
#[derive(Debug, Clone)]
13+
pub struct Database {
14+
pool: SqlitePool,
15+
path: PathBuf,
16+
}
17+
18+
impl Database {
19+
/// Create a new database connection
20+
pub async fn new(path: PathBuf) -> CacheResult<Self> {
21+
// Ensure parent directory exists
22+
if let Some(parent) = path.parent() {
23+
if !parent.exists() {
24+
std::fs::create_dir_all(parent).map_err(|e| {
25+
CacheError::initialization(format!(
26+
"Failed to create database directory: {}",
27+
e
28+
))
29+
})?;
30+
}
31+
}
32+
33+
let db_url = format!("sqlite:{}", path.display());
34+
35+
let options = connection_options(&db_url)?;
36+
37+
// Create database if it doesn't exist
38+
if !sqlx::Sqlite::database_exists(&db_url)
39+
.await
40+
.unwrap_or(false)
41+
{
42+
info!("Creating new database at {}", path.display());
43+
sqlx::Sqlite::create_database(&db_url).await?;
44+
}
45+
46+
let pool = SqlitePoolOptions::new()
47+
.max_connections(5)
48+
.connect_with(options)
49+
.await?;
50+
51+
info!("Connected to database at {}", path.display());
52+
53+
Ok(Self { pool, path })
54+
}
55+
56+
/// Get a reference to the connection pool
57+
pub fn pool(&self) -> &SqlitePool {
58+
&self.pool
59+
}
60+
61+
/// Initialize the database with the given schema
62+
pub async fn init_schema(&self, schema: &str) -> CacheResult<()> {
63+
debug!("Initializing database schema");
64+
sqlx::query(schema).execute(&self.pool).await?;
65+
Ok(())
66+
}
67+
68+
/// Run migrations from the embed_migrations! macro
69+
pub async fn run_migrations<M>(&self, migrations: M) -> CacheResult<()>
70+
where
71+
M: 'static
72+
+ Send
73+
+ Sync
74+
+ Fn(
75+
&SqlitePool,
76+
) -> std::pin::Pin<
77+
Box<dyn std::future::Future<Output = Result<(), sqlx::Error>> + Send>,
78+
>,
79+
{
80+
debug!("Running database migrations");
81+
82+
// Attempt to run migrations
83+
if let Err(err) = migrations(&self.pool).await {
84+
error!("Failed to migrate database: {}", err);
85+
86+
// If the database exists and migrations failed, try to recreate it
87+
if sqlx::Sqlite::database_exists(&format!("sqlite:{}", self.path.display())).await? {
88+
// Close pool and recreate database
89+
self.pool.close().await;
90+
91+
let db_url = format!("sqlite:{}", self.path.display());
92+
sqlx::Sqlite::drop_database(&db_url).await?;
93+
sqlx::Sqlite::create_database(&db_url).await?;
94+
95+
// Reconnect and try migrations again
96+
let options = connection_options(&db_url)?;
97+
let pool = SqlitePoolOptions::new()
98+
.max_connections(5)
99+
.connect_with(options)
100+
.await?;
101+
102+
let new_db = Self {
103+
pool,
104+
path: self.path.clone(),
105+
};
106+
107+
migrations(&new_db.pool).await?;
108+
return Ok(());
109+
}
110+
111+
return Err(err.into());
112+
}
113+
114+
Ok(())
115+
}
116+
117+
/// Close the database connection
118+
pub async fn close(self) {
119+
self.pool.close().await;
120+
}
121+
}
122+
123+
/// Create SQLite connection options
124+
fn connection_options(db_url: &str) -> CacheResult<SqliteConnectOptions> {
125+
let options = SqliteConnectOptions::from_str(db_url)?
126+
.journal_mode(SqliteJournalMode::Wal)
127+
.synchronous(SqliteSynchronous::Normal)
128+
.busy_timeout(Duration::from_secs(10))
129+
.create_if_missing(true)
130+
.foreign_keys(true)
131+
.pragma("wal_autocheckpoint", "1000")
132+
.pragma("journal_size_limit", (64 * 1024 * 1024).to_string()) // 64 MB
133+
.pragma("mmap_size", "134217728") // 128 MB
134+
.pragma("cache_size", "2000"); // 2000 pages
135+
136+
Ok(options)
137+
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use super::*;
142+
use tempfile::TempDir;
143+
144+
#[tokio::test]
145+
async fn test_database_creation() {
146+
let temp_dir = TempDir::new().unwrap();
147+
let db_path = temp_dir.path().join("test.db");
148+
149+
let db = Database::new(db_path.clone()).await.unwrap();
150+
151+
// Test that the database file was created
152+
assert!(db_path.exists());
153+
154+
// Test schema initialization
155+
let schema = "CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)";
156+
db.init_schema(schema).await.unwrap();
157+
158+
// Test that we can execute queries
159+
sqlx::query("INSERT INTO test (name) VALUES (?)")
160+
.bind("test_value")
161+
.execute(db.pool())
162+
.await
163+
.unwrap();
164+
165+
// Test query
166+
let row: (i64, String) = sqlx::query_as("SELECT id, name FROM test WHERE name = ?")
167+
.bind("test_value")
168+
.fetch_one(db.pool())
169+
.await
170+
.unwrap();
171+
172+
assert_eq!(row.1, "test_value");
173+
174+
// Close database
175+
db.close().await;
176+
}
177+
}

devenv-cache-core/src/error.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use miette::Diagnostic;
2+
use std::path::PathBuf;
3+
use thiserror::Error;
4+
5+
/// Common error type for cache operations
6+
#[derive(Error, Diagnostic, Debug)]
7+
pub enum CacheError {
8+
#[error("Database error: {0}")]
9+
Database(#[from] sqlx::Error),
10+
11+
#[error("I/O error: {0}")]
12+
Io(#[from] std::io::Error),
13+
14+
#[error("Failed to initialize cache: {0}")]
15+
Initialization(String),
16+
17+
#[error("JSON serialization error: {0}")]
18+
Json(#[from] serde_json::Error),
19+
20+
#[error("File not found: {0}")]
21+
FileNotFound(PathBuf),
22+
23+
#[error("Environment variable not set: {0}")]
24+
MissingEnvVar(String),
25+
26+
#[error("Invalid path: {0}")]
27+
InvalidPath(PathBuf),
28+
29+
#[error("Content hash calculation failed for {path}: {reason}")]
30+
HashFailure { path: PathBuf, reason: String },
31+
}
32+
33+
impl CacheError {
34+
/// Create a new initialization error
35+
pub fn initialization<S: ToString>(message: S) -> Self {
36+
Self::Initialization(message.to_string())
37+
}
38+
39+
/// Create a new missing environment variable error
40+
pub fn missing_env_var<S: ToString>(var_name: S) -> Self {
41+
Self::MissingEnvVar(var_name.to_string())
42+
}
43+
}
44+
45+
/// A specialized result type for cache operations
46+
pub type CacheResult<T> = std::result::Result<T, CacheError>;

0 commit comments

Comments
 (0)