Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve SQLite connection handling #205

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ Cargo.lock
.aider*
.env
.DS_Store
# Database files
*.db
*.db-*
*.db-shm
*.db-wal
*.sqlite
*.sqlite3

# Backup files
*.bak
*.rs.bak
.tasks
*.new
.vscode/
92 changes: 0 additions & 92 deletions crates/forge_app/src/sqlite.rs

This file was deleted.

36 changes: 36 additions & 0 deletions crates/forge_app/src/sqlite/conn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::time::Duration;

use diesel::r2d2;
use diesel::sqlite::SqliteConnection;

/// Options for customizing SQLite connections
#[derive(Debug)]
pub(crate) struct ConnectionOptions {
busy_timeout: Duration,
}

impl ConnectionOptions {
pub fn new(busy_timeout: Duration) -> Self {
Self { busy_timeout }
}

pub fn default() -> Self {
Self::new(Duration::from_secs(30))
}
}

impl r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error> for ConnectionOptions {
fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> {
use diesel::connection::SimpleConnection;

conn.batch_execute(&format!(
"PRAGMA busy_timeout = {};
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;",
self.busy_timeout.as_millis()
))
.map_err(diesel::r2d2::Error::QueryError)?;

Ok(())
}
}
102 changes: 102 additions & 0 deletions crates/forge_app/src/sqlite/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use anyhow::{Context, Result};
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::sqlite::SqliteConnection;
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
use tracing::debug;

use super::conn::ConnectionOptions;

pub(crate) type SQLConnection = Pool<ConnectionManager<SqliteConnection>>;

const DB_NAME: &str = ".forge.db";
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");

/// SQLite driver that manages database connections and migrations
#[derive(Debug)]
pub(crate) struct Driver {
pool: SQLConnection,
}

impl Driver {
pub fn new(db_path: &str, timeout: Option<std::time::Duration>) -> Result<Self> {
let db_path = format!("{}/{}", db_path, DB_NAME);

// Run migrations first
let mut conn = SqliteConnection::establish(&db_path)?;
let migrations = conn
.run_pending_migrations(MIGRATIONS)
.map_err(|e| anyhow::anyhow!(e))
.with_context(|| "Failed to run database migrations")?;

debug!(
"Running {} migrations for database: {}",
migrations.len(),
db_path
);

drop(conn);

// Create connection pool
let manager = ConnectionManager::<SqliteConnection>::new(db_path);
let options = match timeout {
Some(timeout) => ConnectionOptions::new(timeout),
None => ConnectionOptions::default(),
};

let pool = Pool::builder()
.connection_customizer(Box::new(options))
.max_size(1) // SQLite works better with a single connection
.build(manager)?;

Ok(Driver { pool })
}

pub fn pool(&self) -> SQLConnection {
self.pool.clone()
}
}

#[cfg(test)]
pub(crate) mod tests {
use tempfile::TempDir;

use super::*;

pub struct TestDriver {
driver: Driver,
// Keep TempDir alive for the duration of the test
_temp_dir: TempDir,
}

impl TestDriver {
pub fn new() -> Result<Self> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();

Ok(Self { driver: Driver::new(&db_path, None)?, _temp_dir: temp_dir })
}

pub fn with_timeout(timeout: std::time::Duration) -> Result<Self> {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();

Ok(Self {
driver: Driver::new(&db_path, Some(timeout))?,
_temp_dir: temp_dir,
})
}

pub fn pool(&self) -> SQLConnection {
self.driver.pool()
}
}

#[tokio::test]
async fn test_custom_timeout() -> Result<()> {
let driver = TestDriver::with_timeout(std::time::Duration::from_secs(60))?;
let pool = driver.pool();
assert!(pool.get().is_ok());
Ok(())
}
}
79 changes: 79 additions & 0 deletions crates/forge_app/src/sqlite/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
mod conn;
mod driver;

use anyhow::Result;

use crate::Service;

#[async_trait::async_trait]
pub trait Sqlite: Send + Sync {
async fn pool(&self) -> Result<driver::SQLConnection>;
}

struct Live {
driver: driver::Driver,
}

impl Live {
fn new(db_path: &str, timeout: Option<std::time::Duration>) -> Result<Self> {
Ok(Self { driver: driver::Driver::new(db_path, timeout)? })
}
}

#[async_trait::async_trait]
impl Sqlite for Live {
async fn pool(&self) -> Result<driver::SQLConnection> {
Ok(self.driver.pool())
}
}

impl Service {
/// Create a new SQLite pool service with default timeout (30 seconds)
pub fn db_pool_service(db_path: &str) -> Result<impl Sqlite> {
Live::new(db_path, None)
}

/// Create a new SQLite pool service with custom timeout
pub fn db_pool_service_with_timeout(
db_path: &str,
timeout: std::time::Duration,
) -> Result<impl Sqlite> {
Live::new(db_path, Some(timeout))
}
}

#[cfg(test)]
pub mod tests {
use super::*;

pub struct TestSqlite {
test_driver: driver::tests::TestDriver,
}

impl TestSqlite {
pub fn new() -> Result<Self> {
Ok(Self { test_driver: driver::tests::TestDriver::new()? })
}

pub fn with_timeout(timeout: std::time::Duration) -> Result<Self> {
Ok(Self {
test_driver: driver::tests::TestDriver::with_timeout(timeout)?,
})
}
}

#[async_trait::async_trait]
impl Sqlite for TestSqlite {
async fn pool(&self) -> Result<driver::SQLConnection> {
Ok(self.test_driver.pool())
}
}

#[tokio::test]
async fn test_custom_timeout() -> Result<()> {
let sqlite = TestSqlite::with_timeout(std::time::Duration::from_secs(60))?;
let pool = sqlite.pool().await?;
assert!(pool.get().is_ok());
Ok(())
}
}
Loading