things3_core/database/core.rs
1use crate::{
2 database::pool::{apply_sqlite_optimizations, DatabasePoolConfig},
3 error::{Result as ThingsResult, ThingsError},
4};
5use sqlx::{pool::PoolOptions, SqlitePool};
6use std::path::Path;
7use tracing::{info, instrument};
8
9/// SQLx-based database implementation for Things 3 data
10/// This provides async, Send + Sync compatible database access
11#[derive(Debug, Clone)]
12pub struct ThingsDatabase {
13 pub(crate) pool: SqlitePool,
14 pub(crate) config: DatabasePoolConfig,
15}
16
17impl ThingsDatabase {
18 /// Create a new database connection pool with default configuration
19 ///
20 /// # Examples
21 ///
22 /// ```no_run
23 /// use things3_core::{ThingsDatabase, ThingsError};
24 /// use std::path::Path;
25 ///
26 /// # async fn example() -> Result<(), ThingsError> {
27 /// // Connect to Things 3 database
28 /// let db = ThingsDatabase::new(Path::new("/path/to/things.db")).await?;
29 ///
30 /// // Get inbox tasks
31 /// let tasks = db.get_inbox(None).await?;
32 /// println!("Found {} tasks in inbox", tasks.len());
33 /// # Ok(())
34 /// # }
35 /// ```
36 ///
37 /// # Errors
38 ///
39 /// Returns an error if the database connection fails or if `SQLite` configuration fails
40 #[instrument]
41 pub async fn new(database_path: &Path) -> ThingsResult<Self> {
42 Self::new_with_config(database_path, DatabasePoolConfig::default()).await
43 }
44
45 /// Create a new database connection pool with custom configuration
46 ///
47 /// # Examples
48 ///
49 /// ```no_run
50 /// use things3_core::{ThingsDatabase, DatabasePoolConfig, ThingsError};
51 /// use std::path::Path;
52 /// use std::time::Duration;
53 ///
54 /// # async fn example() -> Result<(), ThingsError> {
55 /// // Create custom pool configuration
56 /// let config = DatabasePoolConfig {
57 /// max_connections: 10,
58 /// min_connections: 2,
59 /// connect_timeout: Duration::from_secs(5),
60 /// idle_timeout: Duration::from_secs(300),
61 /// max_lifetime: Duration::from_secs(3600),
62 /// test_before_acquire: true,
63 /// sqlite_optimizations: Default::default(),
64 /// };
65 ///
66 /// // Connect with custom configuration
67 /// let db = ThingsDatabase::new_with_config(
68 /// Path::new("/path/to/things.db"),
69 /// config,
70 /// ).await?;
71 /// # Ok(())
72 /// # }
73 /// ```
74 ///
75 /// # Errors
76 ///
77 /// Returns an error if the database connection fails or if `SQLite` configuration fails
78 #[instrument]
79 pub async fn new_with_config(
80 database_path: &Path,
81 config: DatabasePoolConfig,
82 ) -> ThingsResult<Self> {
83 let database_url = format!("sqlite:{}", database_path.display());
84
85 info!(
86 "Connecting to SQLite database at: {} with optimized pool",
87 database_url
88 );
89
90 let pool = PoolOptions::new()
91 .max_connections(config.max_connections)
92 .min_connections(config.min_connections)
93 .acquire_timeout(config.connect_timeout)
94 .idle_timeout(Some(config.idle_timeout))
95 .max_lifetime(Some(config.max_lifetime))
96 .test_before_acquire(config.test_before_acquire)
97 .connect(&database_url)
98 .await
99 .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
100
101 apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
102
103 info!(
104 "Database connection pool established successfully with {} max connections",
105 config.max_connections
106 );
107
108 Ok(Self { pool, config })
109 }
110
111 /// Create a new database connection pool from a connection string with default configuration
112 ///
113 /// # Errors
114 ///
115 /// Returns an error if the database connection fails or if `SQLite` configuration fails
116 #[instrument]
117 pub async fn from_connection_string(database_url: &str) -> ThingsResult<Self> {
118 Self::from_connection_string_with_config(database_url, DatabasePoolConfig::default()).await
119 }
120
121 /// Create a new database connection pool from a connection string with custom configuration
122 ///
123 /// # Errors
124 ///
125 /// Returns an error if the database connection fails or if `SQLite` configuration fails
126 #[instrument]
127 pub async fn from_connection_string_with_config(
128 database_url: &str,
129 config: DatabasePoolConfig,
130 ) -> ThingsResult<Self> {
131 info!(
132 "Connecting to SQLite database: {} with optimized pool",
133 database_url
134 );
135
136 let pool = PoolOptions::new()
137 .max_connections(config.max_connections)
138 .min_connections(config.min_connections)
139 .acquire_timeout(config.connect_timeout)
140 .idle_timeout(Some(config.idle_timeout))
141 .max_lifetime(Some(config.max_lifetime))
142 .test_before_acquire(config.test_before_acquire)
143 .connect(database_url)
144 .await
145 .map_err(|e| ThingsError::unknown(format!("Failed to connect to database: {e}")))?;
146
147 apply_sqlite_optimizations(&pool, &config.sqlite_optimizations).await?;
148
149 info!(
150 "Database connection pool established successfully with {} max connections",
151 config.max_connections
152 );
153
154 Ok(Self { pool, config })
155 }
156
157 /// Get the underlying connection pool
158 #[must_use]
159 pub fn pool(&self) -> &SqlitePool {
160 &self.pool
161 }
162}