synd_persistence/sqlite/
connection.rs1use std::{
2 path::Path,
3 time::{Duration, Instant},
4};
5
6use sqlx::{
7 Sqlite, SqlitePool, Transaction,
8 migrate::MigrateError as SqlxMigrateError,
9 sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
10};
11use synd_registry::RegistryDbError;
12use thiserror::Error;
13use tracing::{debug, info};
14
15const SQLITE_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
16const SQLITE_MAX_CONNECTIONS: u32 = 1;
17
18#[derive(Clone)]
19pub struct SqliteDatabase {
20 pool: SqlitePool,
21}
22
23#[derive(Debug, Error)]
25pub enum MigrationError {
26 #[error(transparent)]
27 Sqlx(#[from] SqlxMigrateError),
28}
29
30#[derive(Clone, Copy)]
31enum FileMode {
32 Existing,
33 CreateIfMissing,
34}
35
36impl SqliteDatabase {
37 pub async fn open(db_path: impl AsRef<Path>) -> Result<Self, RegistryDbError> {
38 Self::open_file(db_path, FileMode::Existing).await
39 }
40
41 pub async fn create_or_open(db_path: impl AsRef<Path>) -> Result<Self, RegistryDbError> {
42 Self::open_file(db_path, FileMode::CreateIfMissing).await
43 }
44
45 pub async fn migrate(&self) -> Result<(), MigrationError> {
46 let migrator = sqlx::migrate!("./migrations");
47 let schema_version = migrator
48 .migrations
49 .last()
50 .map_or(0, |migration| migration.version);
51 let started_at = Instant::now();
52 debug!(schema_version, "Running SQLite migrations");
53 migrator.run(&self.pool).await?;
54 info!(
55 schema_version,
56 duration_ms = started_at.elapsed().as_millis(),
57 "SQLite database ready"
58 );
59
60 Ok(())
61 }
62
63 pub async fn begin(&self) -> Result<Transaction<'_, Sqlite>, RegistryDbError> {
64 self.pool.begin().await.map_err(RegistryDbError::retryable)
65 }
66
67 async fn open_file(db_path: impl AsRef<Path>, mode: FileMode) -> Result<Self, RegistryDbError> {
68 let db_path = db_path.as_ref();
69 debug!(
70 database = %db_path.display(),
71 read_only = false,
72 create_if_missing = matches!(mode, FileMode::CreateIfMissing),
73 journal_mode = "wal",
74 foreign_keys = true,
75 busy_timeout_ms = SQLITE_BUSY_TIMEOUT.as_millis(),
76 max_connections = SQLITE_MAX_CONNECTIONS,
77 "Connecting to SQLite"
78 );
79 let opts = Self::file_options(db_path, mode);
80 Self::build_pool(opts).await
81 }
82
83 fn file_options(db_path: impl AsRef<Path>, mode: FileMode) -> SqliteConnectOptions {
84 Self::common_options(
85 SqliteConnectOptions::new()
86 .filename(db_path)
87 .create_if_missing(matches!(mode, FileMode::CreateIfMissing))
88 .journal_mode(SqliteJournalMode::Wal),
89 )
90 }
91
92 fn common_options(opts: SqliteConnectOptions) -> SqliteConnectOptions {
93 opts.foreign_keys(true).busy_timeout(SQLITE_BUSY_TIMEOUT)
94 }
95
96 async fn build_pool(opts: SqliteConnectOptions) -> Result<Self, RegistryDbError> {
97 let database = opts.get_filename().to_owned();
98 let pool = SqlitePoolOptions::new()
99 .max_connections(SQLITE_MAX_CONNECTIONS)
100 .connect_with(opts)
101 .await
102 .map_err(RegistryDbError::retryable)?;
103 debug!(database = %database.display(), "Connected to SQLite");
104
105 Ok(Self { pool })
106 }
107
108 #[cfg(test)]
109 pub async fn in_memory() -> Result<Self, RegistryDbError> {
110 Self::build_pool(Self::common_options(
111 SqliteConnectOptions::new().in_memory(true),
112 ))
113 .await
114 }
115}