microsandbox_db/
connection.rs1use std::{future::Future, path::Path, time::Duration};
15
16use sea_orm::{
17 ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, ExecResult,
18 QueryResult, Statement, TransactionTrait,
19};
20
21use crate::{pool, retry, retry::IsSqliteBusy};
22
23#[derive(Debug, Clone)]
33pub struct DbReadConnection(DatabaseConnection);
34
35#[derive(Debug, Clone)]
45pub struct DbWriteConnection(DatabaseConnection);
46
47impl DbReadConnection {
48 pub fn new(inner: DatabaseConnection) -> Self {
50 Self(inner)
51 }
52
53 pub async fn open(
58 db_path: &Path,
59 max_connections: u32,
60 connect_timeout: Duration,
61 busy_timeout: Duration,
62 ) -> Result<Self, sqlx::Error> {
63 let conn = pool::build_pool(
64 db_path,
65 max_connections,
66 connect_timeout,
67 busy_timeout,
68 false,
69 )
70 .await?;
71 Ok(Self(conn))
72 }
73
74 pub async fn open_read_only(
79 db_path: &Path,
80 connect_timeout: Duration,
81 busy_timeout: Duration,
82 ) -> Result<Self, sqlx::Error> {
83 let options = sqlx::sqlite::SqliteConnectOptions::new()
84 .filename(db_path)
85 .read_only(true)
86 .create_if_missing(false)
87 .busy_timeout(busy_timeout);
88 let pool = sqlx::sqlite::SqlitePoolOptions::new()
89 .max_connections(1)
90 .acquire_timeout(connect_timeout)
91 .connect_with(options)
92 .await?;
93 Ok(Self(sea_orm::SqlxSqliteConnector::from_sqlx_sqlite_pool(
94 pool,
95 )))
96 }
97
98 pub fn inner(&self) -> &DatabaseConnection {
100 &self.0
101 }
102}
103
104impl DbWriteConnection {
105 pub fn new(inner: DatabaseConnection) -> Self {
107 Self(inner)
108 }
109
110 pub async fn open(
115 db_path: &Path,
116 connect_timeout: Duration,
117 busy_timeout: Duration,
118 ) -> Result<Self, sqlx::Error> {
119 let conn = pool::build_pool(db_path, 1, connect_timeout, busy_timeout, true).await?;
120 Ok(Self(conn))
121 }
122
123 pub fn inner(&self) -> &DatabaseConnection {
125 &self.0
126 }
127
128 pub async fn transaction<F, Fut, T, E>(&self, f: F) -> Result<T, E>
144 where
145 F: Fn(DatabaseTransaction) -> Fut,
146 Fut: Future<Output = Result<(DatabaseTransaction, T), E>> + Send,
147 T: Send,
148 E: From<DbErr> + IsSqliteBusy,
149 {
150 retry::retry_on_busy(|| async {
151 let txn = self.0.begin().await?;
152 let (txn, value) = f(txn).await?;
153 txn.commit().await?;
154 Ok(value)
155 })
156 .await
157 }
158}
159
160#[async_trait::async_trait]
161impl ConnectionTrait for DbReadConnection {
162 fn get_database_backend(&self) -> DbBackend {
163 self.0.get_database_backend()
164 }
165
166 async fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
167 self.0.execute_raw(stmt).await
168 }
169
170 async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
171 self.0.execute_unprepared(sql).await
172 }
173
174 async fn query_one_raw(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
175 self.0.query_one_raw(stmt).await
176 }
177
178 async fn query_all_raw(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
179 self.0.query_all_raw(stmt).await
180 }
181
182 fn support_returning(&self) -> bool {
183 self.0.support_returning()
184 }
185
186 fn is_mock_connection(&self) -> bool {
187 self.0.is_mock_connection()
188 }
189}
190
191#[async_trait::async_trait]
204impl ConnectionTrait for DbWriteConnection {
205 fn get_database_backend(&self) -> DbBackend {
206 self.0.get_database_backend()
207 }
208
209 async fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
210 retry::retry_on_busy(|| async { self.0.execute_raw(stmt.clone()).await }).await
211 }
212
213 async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
214 retry::retry_on_busy(|| async { self.0.execute_unprepared(sql).await }).await
215 }
216
217 async fn query_one_raw(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
218 retry::retry_on_busy(|| async { self.0.query_one_raw(stmt.clone()).await }).await
219 }
220
221 async fn query_all_raw(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
222 retry::retry_on_busy(|| async { self.0.query_all_raw(stmt.clone()).await }).await
223 }
224
225 fn support_returning(&self) -> bool {
226 self.0.support_returning()
227 }
228
229 fn is_mock_connection(&self) -> bool {
230 self.0.is_mock_connection()
231 }
232}
233
234#[cfg(test)]
239mod tests {
240 use super::*;
241
242 const TIMEOUT: Duration = Duration::from_secs(5);
243
244 #[tokio::test]
245 async fn strict_reader_sees_wal_commits_but_cannot_write() {
246 let dir = tempfile::tempdir().unwrap();
247 let path = dir.path().join("catalog.db");
248 let writer = DbWriteConnection::open(&path, TIMEOUT, TIMEOUT)
249 .await
250 .unwrap();
251 writer
252 .execute_unprepared("CREATE TABLE control_test (value INTEGER)")
253 .await
254 .unwrap();
255 let reader = DbReadConnection::open_read_only(&path, TIMEOUT, TIMEOUT)
256 .await
257 .unwrap();
258 writer
261 .execute_unprepared("INSERT INTO control_test VALUES (42)")
262 .await
263 .unwrap();
264 let row = reader
265 .query_one_raw(Statement::from_string(
266 DbBackend::Sqlite,
267 "SELECT value FROM control_test",
268 ))
269 .await
270 .unwrap()
271 .unwrap();
272 assert_eq!(row.try_get_by_index::<i64>(0).unwrap(), 42);
273 assert!(
274 reader
275 .execute_unprepared("INSERT INTO control_test VALUES (43)")
276 .await
277 .is_err()
278 );
279 }
280
281 #[tokio::test]
282 async fn strict_reader_never_creates_a_catalog() {
283 let dir = tempfile::tempdir().unwrap();
284 let path = dir.path().join("missing.db");
285 assert!(
286 DbReadConnection::open_read_only(&path, TIMEOUT, TIMEOUT)
287 .await
288 .is_err()
289 );
290 assert!(!path.exists());
291 }
292
293 #[tokio::test]
294 async fn read_open_does_not_create_db() {
295 let dir = tempfile::tempdir().unwrap();
297 let db_path = dir.path().join("catalog.db");
298
299 let result = DbReadConnection::open(&db_path, 1, TIMEOUT, TIMEOUT).await;
300
301 assert!(result.is_err(), "read open should fail on a missing db");
302 assert!(
303 !db_path.exists(),
304 "read open must not create the catalog db file"
305 );
306 }
307
308 #[tokio::test]
309 async fn write_open_creates_db() {
310 let dir = tempfile::tempdir().unwrap();
311 let db_path = dir.path().join("catalog.db");
312
313 let conn = DbWriteConnection::open(&db_path, TIMEOUT, TIMEOUT).await;
314
315 assert!(conn.is_ok(), "write open should succeed");
316 assert!(
317 db_path.exists(),
318 "write open should create the catalog db file"
319 );
320 }
321}