Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/sqlx-sqlite-conn-mgr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ rust-version = "1.89"

[dependencies]
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
15 changes: 9 additions & 6 deletions crates/sqlx-sqlite-conn-mgr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ management.

## Features

* **Single connection pool per database**: Prevents violation of access policies
and/or a glut of open file handles and (mostly) idle threads
* **Maintains one read connection pool and one write connection per database**:
Prevents violation of access policies and/or a glut of open file handles and
(mostly) idle threads
* **Connection pooling**:
* Read-only pool for concurrent reads (up to 6 connections)
* **Lazy write pool**: Single write connection pool (max=1) initialized on
* **Lazy write pool**: Single write connection pool (max_connections=1) initialized on
first use
* **Exclusive write access**: WriteGuard ensures serialized writes
(enforced by max_connections=1)
* **WAL mode**: Automatically enabled on first `acquire_writer()` call
(idempotent)
* See [WAL documentation](https://www.sqlite.org/wal.html) for details
* **30-second idle timeout**: Both read and write connections close after
30 seconds of inactivity
* **No perpetual caching**: Zero minimum connections (min_connections=0) to
* **No perpetual connection caching**: Zero minimum connections (min_connections=0) to
avoid idle thread overhead

## Design Philosophy
Expand Down Expand Up @@ -110,9 +111,11 @@ queries.
is released via `WriteGuard` drop.

5. **Connection Management**:
* Read pool: max 6 concurrent connections, 0 cached
* Read pool: 6 concurrent connections by default, 0 cached
* Can be configured via `SqliteDatabaseConfig`
* Write pool: max 1 connection, 0 cached
* Idle timeout: 30 seconds for both pools
* Can be configured via `SqliteDatabaseConfig`
* No perpetual caching to minimize idle thread overhead

## Error Handling
Expand Down
54 changes: 54 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Configuration for SQLite database connection pools

use std::time::Duration;

/// Configuration for SqliteDatabase connection pools
///
/// # Examples
///
/// ```
/// use sqlx_sqlite_conn_mgr::SqliteDatabaseConfig;
/// use std::time::Duration;
///
/// // Use defaults
/// let config = SqliteDatabaseConfig::default();
///
/// // Customize specific fields
/// let config = SqliteDatabaseConfig {
/// max_read_connections: 3,
/// idle_timeout: Duration::from_secs(60),
/// };
///
/// // Override just one field
/// let config = SqliteDatabaseConfig {
/// max_read_connections: 3,
/// ..Default::default()
/// };
/// ```
#[derive(Debug, Clone)]
pub struct SqliteDatabaseConfig {
/// Maximum number of concurrent read connections
///
/// This controls the size of the read-only connection pool.
/// Higher values allow more concurrent read queries but consume more resources.
///
/// Default: 6
pub max_read_connections: u32,

/// Idle timeout for both read and write connections
///
/// Connections that remain idle for this duration will be closed automatically.
/// This helps prevent resource exhaustion from idle threads.
///
/// Default: 30 seconds
pub idle_timeout: Duration,
}

impl Default for SqliteDatabaseConfig {
fn default() -> Self {
Self {
max_read_connections: 6,
idle_timeout: Duration::from_secs(30),
}
}
}
45 changes: 45 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/database.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! SQLite database with connection pooling and optional write access

use sqlx::{Pool, Sqlite};
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;

/// SQLite database with connection pooling for concurrent reads and optional exclusive writes.
///
/// ## Architecture
///
/// The database maintains two connection pools:
/// - **`read_pool`**: Pool of read-only connections for concurrent reads
Comment thread
pmorris-dev marked this conversation as resolved.
/// - **`write_conn`**: Single-connection pool for exclusive write access (enforced by max_connections=1)
///
/// ## State Management
///
/// - **`wal_initialized`**: Tracks whether WAL journal mode has been enabled (lazy initialization)
/// - **`closed`**: Prevents use after the database has been closed
/// - **`path`**: Database file path for cleanup operations
///
/// ## Usage Pattern
///
/// ```text
/// 1. Connect to database (creates/reuses connection pools)
/// 2. Read operations: Access read_pool for concurrent reads
/// 3. Write operations: Acquire writer (lazily enables WAL on first call)
/// 4. Close database when done
/// ```
#[derive(Debug)]
pub struct SqliteDatabase {
Comment thread
pmorris-dev marked this conversation as resolved.
/// Pool of read-only connections (defaults to max_connections=6) for concurrent reads
read_pool: Pool<Sqlite>,

/// Single read-write connection pool (max_connections=1) for serialized writes
write_conn: Pool<Sqlite>,

/// Tracks if WAL mode has been initialized (set on first write)
wal_initialized: AtomicBool,
Comment thread
pmorris-dev marked this conversation as resolved.

/// Marks database as closed to prevent further operations
closed: AtomicBool,

/// Path to database file (used for cleanup and registry lookups)
path: PathBuf,
}
23 changes: 23 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! Error types for sqlx-sqlite-conn-mgr

use thiserror::Error;

/// Errors that may occur when working with sqlx-sqlite-conn-mgr
#[derive(Error, Debug)]
pub enum Error {
/// IO error when accessing database files. Standard library IO errors
/// are converted to this variant.
#[error("IO error: {0}")]
Io(#[from] std::io::Error),

/// Error from the sqlx library. Standard sqlx errors are converted to this variant
#[error("Sqlx error: {0}")]
Sqlx(#[from] sqlx::Error),

/// Database has been closed and cannot be used
#[error("Database has been closed")]
DatabaseClosed,
}

/// A type alias for Results with our Error type
pub type Result<T> = std::result::Result<T, Error>;
34 changes: 33 additions & 1 deletion crates/sqlx-sqlite-conn-mgr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
//! # SQLx Connection Pool Manager
//! # sqlx-sqlite-conn-mgr
//!
//! A minimal wrapper around SQLx that enforces pragmatic SQLite connection policies
//! for mobile and desktop applications.
//!
//! ## Core Types
//!
//! - **[`SqliteDatabase`]**: Main database type with separate read and write connection pools
//! - **[`SqliteDatabaseConfig`]**: Configuration for connection pool settings
//! - **[`WriteGuard`]**: RAII guard ensuring exclusive write access
//! - **[`Error`]**: Error type for database operations
//!
//! ## Architecture
//!
//! - **Dual pools**: Separate read-only pool (max 6 connections) and write pool (max 1 connection)
//! - **Lazy WAL mode**: Write-Ahead Logging enabled automatically on first write
//! - **Exclusive writes**: Single-connection write pool enforces serialized write access
//! - **Concurrent reads**: Multiple readers can query simultaneously via the read pool

// TODO: Remove these allows once implementation is complete
#![allow(dead_code)]
#![allow(unused)]

mod config;
mod database;
mod error;
mod write_guard;

// Re-export public types
pub use config::SqliteDatabaseConfig;
pub use database::SqliteDatabase;
pub use error::{Error, Result};
pub use write_guard::WriteGuard;
62 changes: 62 additions & 0 deletions crates/sqlx-sqlite-conn-mgr/src/write_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! WriteGuard for exclusive write access to the database

use sqlx::Sqlite;
use sqlx::pool::PoolConnection;
use sqlx::sqlite::SqliteConnection;
use std::ops::{Deref, DerefMut};

/// RAII guard for exclusive write access to a database connection
///
/// This guard wraps a pool connection and returns it to the pool on drop.
/// Only one `WriteGuard` can exist at a time (enforced by max_connections=1),
/// ensuring serialized write access.
///
/// The guard derefs to `SqliteConnection` allowing direct use with sqlx queries.
///
/// # Example
/// TODO: Remove ignore once implementation is complete
/// ```ignore
/// use sqlx_sqlite_conn_mgr::SqliteDatabase;
/// use sqlx::query;
///
/// # async fn example() -> Result<(), sqlx_sqlite_conn_mgr::Error> {
/// let db = SqliteDatabase::connect("test.db").await?;
/// let mut writer = db.acquire_writer().await?;
/// // Use &mut *writer for write queries (e.g. INSERT/UPDATE/DELETE)
/// query("INSERT INTO users (name) VALUES (?)")
/// .bind("Alice")
/// .execute(&mut *writer)
/// .await?;
/// // Writer is automatically returned when dropped
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct WriteGuard {
conn: PoolConnection<Sqlite>,
}

impl WriteGuard {
/// Create a new WriteGuard by taking ownership of a pool connection
pub(crate) fn new(conn: PoolConnection<Sqlite>) -> Self {
Self { conn }
}
}

impl Deref for WriteGuard {
type Target = SqliteConnection;

fn deref(&self) -> &Self::Target {
&*self.conn
}
}

impl DerefMut for WriteGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut *self.conn
}
}

// Drop is automatically implemented - PoolConnection returns itself to the pool

// WriteGuard is automatically Send because PoolConnection<Sqlite> is Send