From 173a29611d5acffe39b9731bbdb4b03e46b320b1 Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:20:59 -0500 Subject: [PATCH] feat: add types to show API shape and design - Once we have consensus on the API and types, I'll move on to implementation details and tests - Have to allow dead code and unused right now until we fully implement and test the API - Have to ignore doc tests temporarily for the same reason --- Cargo.lock | 1 + crates/sqlx-sqlite-conn-mgr/Cargo.toml | 1 + crates/sqlx-sqlite-conn-mgr/README.md | 15 +++-- crates/sqlx-sqlite-conn-mgr/src/config.rs | 54 ++++++++++++++++ crates/sqlx-sqlite-conn-mgr/src/database.rs | 45 ++++++++++++++ crates/sqlx-sqlite-conn-mgr/src/error.rs | 23 +++++++ crates/sqlx-sqlite-conn-mgr/src/lib.rs | 34 +++++++++- .../sqlx-sqlite-conn-mgr/src/write_guard.rs | 62 +++++++++++++++++++ 8 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 crates/sqlx-sqlite-conn-mgr/src/config.rs create mode 100644 crates/sqlx-sqlite-conn-mgr/src/database.rs create mode 100644 crates/sqlx-sqlite-conn-mgr/src/error.rs create mode 100644 crates/sqlx-sqlite-conn-mgr/src/write_guard.rs diff --git a/Cargo.lock b/Cargo.lock index 382219c..37e1cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3574,6 +3574,7 @@ name = "sqlx-sqlite-conn-mgr" version = "0.8.6" dependencies = [ "sqlx", + "thiserror 2.0.17", "tokio", ] diff --git a/crates/sqlx-sqlite-conn-mgr/Cargo.toml b/crates/sqlx-sqlite-conn-mgr/Cargo.toml index 7ed6fe7..d204e27 100644 --- a/crates/sqlx-sqlite-conn-mgr/Cargo.toml +++ b/crates/sqlx-sqlite-conn-mgr/Cargo.toml @@ -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"] } diff --git a/crates/sqlx-sqlite-conn-mgr/README.md b/crates/sqlx-sqlite-conn-mgr/README.md index d42ee96..670a79b 100644 --- a/crates/sqlx-sqlite-conn-mgr/README.md +++ b/crates/sqlx-sqlite-conn-mgr/README.md @@ -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 @@ -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 diff --git a/crates/sqlx-sqlite-conn-mgr/src/config.rs b/crates/sqlx-sqlite-conn-mgr/src/config.rs new file mode 100644 index 0000000..c90d719 --- /dev/null +++ b/crates/sqlx-sqlite-conn-mgr/src/config.rs @@ -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), + } + } +} diff --git a/crates/sqlx-sqlite-conn-mgr/src/database.rs b/crates/sqlx-sqlite-conn-mgr/src/database.rs new file mode 100644 index 0000000..832ed2f --- /dev/null +++ b/crates/sqlx-sqlite-conn-mgr/src/database.rs @@ -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 +/// - **`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 { + /// Pool of read-only connections (defaults to max_connections=6) for concurrent reads + read_pool: Pool, + + /// Single read-write connection pool (max_connections=1) for serialized writes + write_conn: Pool, + + /// Tracks if WAL mode has been initialized (set on first write) + wal_initialized: AtomicBool, + + /// Marks database as closed to prevent further operations + closed: AtomicBool, + + /// Path to database file (used for cleanup and registry lookups) + path: PathBuf, +} diff --git a/crates/sqlx-sqlite-conn-mgr/src/error.rs b/crates/sqlx-sqlite-conn-mgr/src/error.rs new file mode 100644 index 0000000..a6608ca --- /dev/null +++ b/crates/sqlx-sqlite-conn-mgr/src/error.rs @@ -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 = std::result::Result; diff --git a/crates/sqlx-sqlite-conn-mgr/src/lib.rs b/crates/sqlx-sqlite-conn-mgr/src/lib.rs index b541b8e..b29c663 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/lib.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/lib.rs @@ -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; diff --git a/crates/sqlx-sqlite-conn-mgr/src/write_guard.rs b/crates/sqlx-sqlite-conn-mgr/src/write_guard.rs new file mode 100644 index 0000000..4ac1f66 --- /dev/null +++ b/crates/sqlx-sqlite-conn-mgr/src/write_guard.rs @@ -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, +} + +impl WriteGuard { + /// Create a new WriteGuard by taking ownership of a pool connection + pub(crate) fn new(conn: PoolConnection) -> 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 is Send