Skip to main content

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}