1pub mod migrations;
2pub mod runtime;
3pub mod types;
4
5use anyhow::Error;
6use runtime::RUNTIME;
7use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
8use std::path::Path;
9use std::str::FromStr;
10
11pub const DB_FILENAME: &str = "cadmus.sqlite";
13
14#[derive(Clone)]
18pub struct Database {
19 pool: SqlitePool,
20}
21
22impl std::fmt::Debug for Database {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 f.debug_struct("Database").finish()
25 }
26}
27
28impl Database {
29 #[cfg_attr(feature = "tracing", tracing::instrument(fields(db_path = %path.as_ref().display())))]
40 pub fn new<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, Error> {
41 let path = path.as_ref();
42 if let Some(parent) = path.parent()
43 && !parent.as_os_str().is_empty()
44 {
45 std::fs::create_dir_all(parent)?;
46 }
47
48 let path_str = path.display().to_string();
49
50 tracing::info!(db_path = %path_str, "connecting to database");
51
52 RUNTIME.block_on(async {
53 let options = SqliteConnectOptions::from_str(&format!("sqlite://{}", path_str))?
54 .create_if_missing(true)
55 .foreign_keys(true);
56
57 let pool = SqlitePoolOptions::new()
58 .max_connections(5)
59 .connect_with(options)
60 .await?;
61
62 tracing::info!(db_path = %path_str, "database connected");
63 Ok(Database { pool })
64 })
65 }
66
67 pub fn close(&self) {
73 tracing::info!("closing database connection pool");
74 RUNTIME.block_on(async {
75 self.pool.close().await;
76 });
77 tracing::info!("database connection pool closed");
78 }
79
80 pub fn pool(&self) -> &SqlitePool {
82 &self.pool
83 }
84
85 pub fn migration_runner(&self) -> migrations::MigrationRunner {
90 migrations::MigrationRunner::new(self.pool.clone())
91 }
92
93 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
98 pub fn migrate(&self) -> Result<(), Error> {
99 RUNTIME.block_on(async {
100 tracing::info!("running schema migrations");
101 #[cfg(feature = "tracing")]
102 let span = tracing::info_span!("sqlx_migrations").entered();
103 sqlx::migrate!("./migrations").run(&self.pool).await?;
104 #[cfg(feature = "tracing")]
105 span.exit();
106
107 tracing::info!("running runtime migrations");
108 self.migration_runner().run_all().await
109 })
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_database_creation() {
119 let db = Database::new(":memory:").expect("failed to create in-memory database");
120 db.migrate().expect("failed to run migrations");
121
122 RUNTIME.block_on(async {
123 let result: (i64,) = sqlx::query_as(
124 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='books'",
125 )
126 .fetch_one(&db.pool)
127 .await
128 .expect("failed to query sqlite_master");
129
130 assert_eq!(result.0, 1, "books table should exist after migrations");
131 });
132 }
133}