1#![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}