Skip to main content

minco_sqlx_sqlite/
lib.rs

1//! `SQLx` `SQLite` pools with explicit file-backed versus in-memory behavior.
2#![forbid(unsafe_code)]
3
4use serde::{Deserialize, Serialize};
5pub use sqlx::SqlitePool;
6use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
7use std::{path::Path, str::FromStr, time::Duration};
8use thiserror::Error;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct SqlitePoolConfig {
12    pub url: String,
13    pub max_connections: u32,
14    pub acquire_timeout_seconds: u64,
15}
16
17impl SqlitePoolConfig {
18    pub fn file(path: impl AsRef<Path>) -> Self {
19        Self {
20            url: format!("sqlite://{}", path.as_ref().display()),
21            max_connections: 4,
22            acquire_timeout_seconds: 5,
23        }
24    }
25    pub fn memory() -> Self {
26        Self {
27            url: "sqlite::memory:".into(),
28            max_connections: 1,
29            acquire_timeout_seconds: 5,
30        }
31    }
32    pub fn is_memory(&self) -> bool {
33        self.url == "sqlite::memory:" || self.url.contains("mode=memory")
34    }
35    pub fn validate(&self) -> Result<(), SqliteError> {
36        if self.url.trim().is_empty() {
37            return Err(SqliteError::InvalidConfig("database URL is empty".into()));
38        }
39        if self.max_connections == 0 {
40            return Err(SqliteError::InvalidConfig(
41                "max_connections must be at least 1".into(),
42            ));
43        }
44        if self.is_memory() && self.max_connections != 1 {
45            return Err(SqliteError::InvalidConfig(
46                "in-memory SQLite requires exactly one pooled connection".into(),
47            ));
48        }
49        Ok(())
50    }
51}
52
53pub async fn connect(config: &SqlitePoolConfig) -> Result<SqlitePool, SqliteError> {
54    config.validate()?;
55    let mut options = SqliteConnectOptions::from_str(&config.url)?
56        .create_if_missing(!config.is_memory())
57        .foreign_keys(true);
58    if !config.is_memory() {
59        options = options.journal_mode(SqliteJournalMode::Wal);
60    }
61    Ok(SqlitePoolOptions::new()
62        .max_connections(config.max_connections)
63        .acquire_timeout(Duration::from_secs(config.acquire_timeout_seconds))
64        .connect_with(options)
65        .await?)
66}
67
68pub async fn migrate(pool: &SqlitePool, path: impl AsRef<Path>) -> Result<(), SqliteError> {
69    let migrator = sqlx::migrate::Migrator::new(path.as_ref()).await?;
70    migrator.run(pool).await?;
71    Ok(())
72}
73
74pub async fn ready(pool: &SqlitePool) -> bool {
75    matches!(
76        sqlx::query_scalar::<_, i64>("SELECT 1")
77            .fetch_one(pool)
78            .await,
79        Ok(1)
80    )
81}
82
83#[derive(Debug, Error)]
84pub enum SqliteError {
85    #[error("invalid SQLite configuration: {0}")]
86    InvalidConfig(String),
87    #[error("SQLite error: {0}")]
88    Sqlx(#[from] sqlx::Error),
89    #[error("SQLite migration error: {0}")]
90    Migration(#[from] sqlx::migrate::MigrateError),
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    #[test]
97    fn memory_profile_rejects_multiple_connections() {
98        let mut config = SqlitePoolConfig::memory();
99        config.max_connections = 2;
100        assert!(config.validate().is_err());
101    }
102}