Skip to main content

rustrails_record/
connection.rs

1use rustrails_support::runtime;
2use sea_orm::{ConnectOptions, Database, DatabaseConnection};
3
4/// Errors returned while establishing or managing database connections.
5#[derive(Debug, thiserror::Error)]
6pub enum ConnectionError {
7    /// The database connection could not be established.
8    #[error("connection failed: {0}")]
9    ConnectionFailed(String),
10    /// No database connection is available.
11    #[error("not connected")]
12    NotConnected,
13}
14
15/// Wrapper around a SeaORM database connection.
16#[derive(Clone, Debug)]
17pub struct ConnectionPool {
18    db: DatabaseConnection,
19}
20
21impl ConnectionPool {
22    /// Establishes a database connection from a URL.
23    pub async fn connect(url: &str) -> Result<Self, ConnectionError> {
24        let db = Database::connect(url)
25            .await
26            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))?;
27        Ok(Self { db })
28    }
29
30    /// Synchronous wrapper for [`Self::connect`].
31    pub fn connect_sync(url: &str) -> Result<Self, ConnectionError> {
32        runtime::block_on(Self::connect(url))
33    }
34
35    /// Establishes a database connection using explicit connection options.
36    pub async fn connect_with_options(options: ConnectOptions) -> Result<Self, ConnectionError> {
37        let db = Database::connect(options)
38            .await
39            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))?;
40        Ok(Self { db })
41    }
42
43    /// Synchronous wrapper for [`Self::connect_with_options`].
44    pub fn connect_with_options_sync(options: ConnectOptions) -> Result<Self, ConnectionError> {
45        runtime::block_on(Self::connect_with_options(options))
46    }
47
48    /// Returns the underlying SeaORM connection.
49    pub fn connection(&self) -> &DatabaseConnection {
50        &self.db
51    }
52
53    /// Executes a simple query to confirm the connection is healthy.
54    pub async fn ping(&self) -> Result<(), ConnectionError> {
55        self.db
56            .ping()
57            .await
58            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))
59    }
60
61    /// Synchronous wrapper for [`Self::ping`].
62    pub fn ping_sync(&self) -> Result<(), ConnectionError> {
63        runtime::block_on(self.ping())
64    }
65
66    /// Closes the underlying database connection.
67    pub async fn close(self) -> Result<(), ConnectionError> {
68        self.db
69            .close()
70            .await
71            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))
72    }
73
74    /// Synchronous wrapper for [`Self::close`].
75    pub fn close_sync(self) -> Result<(), ConnectionError> {
76        runtime::block_on(self.close())
77    }
78}
79
80/// Establishes a database connection from a URL string.
81pub async fn establish(url: &str) -> Result<ConnectionPool, ConnectionError> {
82    ConnectionPool::connect(url).await
83}
84
85/// Synchronous wrapper for [`establish`].
86pub fn establish_sync(url: &str) -> Result<ConnectionPool, ConnectionError> {
87    runtime::block_on(establish(url))
88}
89
90#[cfg(test)]
91mod tests {
92    use rustrails_support::{database, runtime};
93    use sea_orm::{ConnectOptions, DatabaseBackend};
94
95    use super::{ConnectionError, ConnectionPool, establish, establish_sync};
96
97    fn run_sync_connection_test(test: impl FnOnce() + Send + 'static) {
98        std::thread::spawn(move || {
99            let _rt = runtime::init_runtime();
100            database::establish("sqlite::memory:")
101                .expect("sqlite in-memory connection should succeed");
102            test();
103        })
104        .join()
105        .unwrap();
106    }
107
108    #[tokio::test]
109    async fn connect_to_in_memory_sqlite() {
110        let pool = ConnectionPool::connect("sqlite::memory:")
111            .await
112            .expect("sqlite in-memory connection should succeed");
113
114        assert_eq!(
115            pool.connection().get_database_backend(),
116            DatabaseBackend::Sqlite
117        );
118    }
119
120    #[tokio::test]
121    async fn connect_with_options_uses_same_backend() {
122        let mut options = ConnectOptions::new("sqlite::memory:");
123        options.sqlx_logging(false);
124
125        let pool = ConnectionPool::connect_with_options(options)
126            .await
127            .expect("sqlite in-memory connection should succeed");
128
129        assert_eq!(
130            pool.connection().get_database_backend(),
131            DatabaseBackend::Sqlite
132        );
133    }
134
135    #[tokio::test]
136    async fn ping_succeeds_for_live_connection() {
137        let pool = establish("sqlite::memory:")
138            .await
139            .expect("sqlite in-memory connection should succeed");
140
141        pool.ping().await.expect("ping should succeed");
142    }
143
144    #[tokio::test]
145    async fn close_succeeds_for_live_connection() {
146        let pool = establish("sqlite::memory:")
147            .await
148            .expect("sqlite in-memory connection should succeed");
149
150        pool.close().await.expect("close should succeed");
151    }
152
153    #[test]
154    fn connect_sync_to_in_memory_sqlite() {
155        run_sync_connection_test(|| {
156            let pool = ConnectionPool::connect_sync("sqlite::memory:")
157                .expect("sqlite in-memory connection should succeed");
158
159            assert_eq!(
160                pool.connection().get_database_backend(),
161                DatabaseBackend::Sqlite
162            );
163        });
164    }
165
166    #[test]
167    fn connect_with_options_sync_uses_same_backend() {
168        run_sync_connection_test(|| {
169            let mut options = ConnectOptions::new("sqlite::memory:");
170            options.sqlx_logging(false);
171
172            let pool = ConnectionPool::connect_with_options_sync(options)
173                .expect("sqlite in-memory connection should succeed");
174
175            assert_eq!(
176                pool.connection().get_database_backend(),
177                DatabaseBackend::Sqlite
178            );
179        });
180    }
181
182    #[test]
183    fn ping_sync_succeeds_for_live_connection() {
184        run_sync_connection_test(|| {
185            let pool = establish_sync("sqlite::memory:")
186                .expect("sqlite in-memory connection should succeed");
187
188            pool.ping_sync().expect("ping should succeed");
189        });
190    }
191
192    #[test]
193    fn close_sync_succeeds_for_live_connection() {
194        run_sync_connection_test(|| {
195            let pool = establish_sync("sqlite::memory:")
196                .expect("sqlite in-memory connection should succeed");
197
198            pool.close_sync().expect("close should succeed");
199        });
200    }
201
202    #[test]
203    fn establish_sync_creates_connection_pool() {
204        run_sync_connection_test(|| {
205            let pool = establish_sync("sqlite::memory:")
206                .expect("sqlite in-memory connection should succeed");
207
208            assert_eq!(
209                pool.connection().get_database_backend(),
210                DatabaseBackend::Sqlite
211            );
212        });
213    }
214
215    #[tokio::test]
216    async fn invalid_url_returns_connection_error() {
217        let result = ConnectionPool::connect("not-a-valid-database-url").await;
218
219        assert!(matches!(result, Err(ConnectionError::ConnectionFailed(_))));
220    }
221}