1use std::num::NonZeroUsize;
10use std::path::{Path, PathBuf};
11
12use deadpool::Runtime;
13use deadpool::managed::{Manager, Metrics, Object, Pool, RecycleError, RecycleResult};
14use deadpool_sync::SyncWrapper;
15use rusqlite::{Connection, OpenFlags, TransactionBehavior};
16use tracing::Instrument;
17
18use crate::sqlite::tx::{ReadTx, WriteTx};
19use crate::{DatabaseError, default_connection_pool_size};
20
21const STATEMENT_CACHE_CAPACITY: usize = 512;
25
26#[derive(Debug, thiserror::Error)]
34pub(crate) enum SqliteManagerError {
35 #[error("failed to open the sqlite database")]
37 Open(#[source] rusqlite::Error),
38 #[error("failed to configure the sqlite connection")]
40 Configure(#[source] rusqlite::Error),
41 #[error("the pooled sqlite connection is poisoned")]
43 Poisoned,
44}
45
46struct SqliteManager {
47 path: PathBuf,
48 read_only: bool,
51}
52
53impl Manager for SqliteManager {
54 type Type = SyncWrapper<Connection>;
55 type Error = SqliteManagerError;
56
57 async fn create(&self) -> Result<Self::Type, Self::Error> {
58 let path = self.path.clone();
59 let read_only = self.read_only;
60 SyncWrapper::new(Runtime::Tokio1, move || {
61 let conn = Connection::open_with_flags(&path, OpenFlags::SQLITE_OPEN_READ_WRITE)
62 .map_err(SqliteManagerError::Open)?;
63 configure_connection(&conn, read_only).map_err(SqliteManagerError::Configure)?;
64 Ok(conn)
65 })
66 .await
67 }
68
69 async fn recycle(
70 &self,
71 conn: &mut Self::Type,
72 _metrics: &Metrics,
73 ) -> RecycleResult<Self::Error> {
74 if conn.is_mutex_poisoned() {
75 return Err(RecycleError::Backend(SqliteManagerError::Poisoned));
76 }
77 conn.interact(|conn| {
80 if !conn.is_autocommit() {
81 let _ = conn.execute_batch("ROLLBACK");
82 }
83 })
84 .await
85 .map_err(|_| RecycleError::Backend(SqliteManagerError::Poisoned))?;
86 Ok(())
87 }
88}
89
90fn configure_connection(conn: &Connection, read_only: bool) -> rusqlite::Result<()> {
96 if read_only {
99 conn.execute_batch(
102 "PRAGMA busy_timeout = 5000;
103 PRAGMA foreign_keys = ON;
104 PRAGMA query_only = ON;",
105 )?;
106 } else {
107 conn.execute_batch(
109 "PRAGMA busy_timeout = 5000;
110 PRAGMA journal_mode = WAL;
111 PRAGMA foreign_keys = ON;",
112 )?;
113 }
114 conn.set_prepared_statement_cache_capacity(STATEMENT_CACHE_CAPACITY);
115 rusqlite::vtab::array::load_module(conn)?;
118 Ok(())
119}
120
121#[derive(Clone)]
128pub struct Database {
129 writer: Pool<SqliteManager>,
130 readers: Pool<SqliteManager>,
131}
132
133impl Database {
134 pub fn new(database_filepath: &Path) -> Result<Self, DatabaseError> {
136 Self::new_with_pool_size(database_filepath, default_connection_pool_size())
137 }
138
139 pub fn new_with_pool_size(
142 database_filepath: &Path,
143 connection_pool_size: NonZeroUsize,
144 ) -> Result<Self, DatabaseError> {
145 let writer = Pool::builder(SqliteManager {
146 path: database_filepath.to_path_buf(),
147 read_only: false,
148 })
149 .max_size(1)
150 .build()?;
151 let readers = Pool::builder(SqliteManager {
152 path: database_filepath.to_path_buf(),
153 read_only: true,
154 })
155 .max_size(connection_pool_size.get())
156 .build()?;
157 Ok(Self { writer, readers })
158 }
159
160 async fn checkout_writer(&self) -> Result<Object<SqliteManager>, DatabaseError> {
162 self.writer
163 .get()
164 .in_current_span()
165 .await
166 .map_err(|err| DatabaseError::ConnectionPoolObtainError(Box::new(err)))
167 }
168
169 async fn checkout_reader(&self) -> Result<Object<SqliteManager>, DatabaseError> {
171 self.readers
172 .get()
173 .in_current_span()
174 .await
175 .map_err(|err| DatabaseError::ConnectionPoolObtainError(Box::new(err)))
176 }
177
178 pub async fn read<R, E, F>(&self, msg: impl ToString + Send, query: F) -> Result<R, E>
181 where
182 F: FnOnce(&ReadTx<'_>) -> Result<R, E> + Send + 'static,
183 R: Send + 'static,
184 E: From<DatabaseError> + Send + 'static,
185 {
186 let conn = self.checkout_reader().await.map_err(E::from)?;
187 let msg = msg.to_string();
188 let span = tracing::Span::current();
189 conn.interact(move |conn| {
190 let _guard = span.enter();
191 let tx = conn
192 .transaction_with_behavior(TransactionBehavior::Deferred)
193 .map_err(|err| E::from(DatabaseError::from(err)))?;
194 query(&ReadTx::new(&tx))
195 })
197 .await
198 .map_err(|err| E::from(DatabaseError::interact(&msg, &err)))?
199 }
200
201 pub async fn write<R, E, F>(&self, msg: impl ToString + Send, query: F) -> Result<R, E>
204 where
205 F: FnOnce(&WriteTx<'_>) -> Result<R, E> + Send + 'static,
206 R: Send + 'static,
207 E: From<DatabaseError> + Send + 'static,
208 {
209 let conn = self.checkout_writer().await.map_err(E::from)?;
210 let msg = msg.to_string();
211 let span = tracing::Span::current();
212 conn.interact(move |conn| {
213 let _guard = span.enter();
214 let tx = conn
215 .transaction_with_behavior(TransactionBehavior::Immediate)
216 .map_err(|err| E::from(DatabaseError::from(err)))?;
217 let result = query(&WriteTx::new(&tx))?;
218 tx.commit().map_err(|err| E::from(DatabaseError::from(err)))?;
219 Ok(result)
220 })
221 .await
222 .map_err(|err| E::from(DatabaseError::interact(&msg, &err)))?
223 }
224
225 pub async fn begin_read(&self) -> Result<ReadTransaction, DatabaseError> {
228 let conn = self.checkout_reader().await?;
229 run_tx_stmt(&conn, "BEGIN DEFERRED").await?;
230 Ok(ReadTransaction { conn })
231 }
232
233 pub async fn begin_write(&self) -> Result<WriteTransaction, DatabaseError> {
237 let conn = self.checkout_writer().await?;
238 run_tx_stmt(&conn, "BEGIN IMMEDIATE").await?;
239 Ok(WriteTransaction { conn })
240 }
241}
242
243async fn run_tx_stmt(
248 conn: &Object<SqliteManager>,
249 stmt: &'static str,
250) -> Result<(), DatabaseError> {
251 conn.interact(move |conn| conn.execute_batch(stmt))
252 .await
253 .map_err(|err| DatabaseError::interact(stmt, &err))?
254 .map_err(DatabaseError::from)
255}
256
257pub struct ReadTransaction {
264 conn: Object<SqliteManager>,
265}
266
267impl ReadTransaction {
268 pub async fn run<R, E, F>(&self, msg: impl ToString + Send, query: F) -> Result<R, E>
270 where
271 F: FnOnce(&ReadTx<'_>) -> Result<R, E> + Send + 'static,
272 R: Send + 'static,
273 E: From<DatabaseError> + Send + 'static,
274 {
275 let msg = msg.to_string();
276 let span = tracing::Span::current();
277 self.conn
278 .interact(move |conn| {
279 let _guard = span.enter();
280 query(&ReadTx::new(conn))
281 })
282 .await
283 .map_err(|err| E::from(DatabaseError::interact(&msg, &err)))?
284 }
285
286 pub async fn close(self) -> Result<(), DatabaseError> {
288 run_tx_stmt(&self.conn, "ROLLBACK").await
289 }
290}
291
292pub struct WriteTransaction {
303 conn: Object<SqliteManager>,
304}
305
306impl WriteTransaction {
307 pub async fn run<R, E, F>(&self, msg: impl ToString + Send, query: F) -> Result<R, E>
309 where
310 F: FnOnce(&WriteTx<'_>) -> Result<R, E> + Send + 'static,
311 R: Send + 'static,
312 E: From<DatabaseError> + Send + 'static,
313 {
314 let msg = msg.to_string();
315 let span = tracing::Span::current();
316 self.conn
317 .interact(move |conn| {
318 let _guard = span.enter();
319 query(&WriteTx::new(conn))
320 })
321 .await
322 .map_err(|err| E::from(DatabaseError::interact(&msg, &err)))?
323 }
324
325 pub async fn commit(self) -> Result<(), DatabaseError> {
327 run_tx_stmt(&self.conn, "COMMIT").await
328 }
329
330 pub async fn rollback(self) -> Result<(), DatabaseError> {
332 run_tx_stmt(&self.conn, "ROLLBACK").await
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use std::num::NonZeroUsize;
339 use std::path::{Path, PathBuf};
340
341 use rusqlite::Connection;
342
343 use super::Database;
344 use crate::DatabaseError;
345
346 struct TempDb {
349 path: PathBuf,
350 }
351
352 impl TempDb {
353 fn new(name: &str) -> Self {
354 let path = std::env::temp_dir()
355 .join(format!("miden-node-db-pool-{name}-{}.sqlite3", std::process::id()));
356 let db = Self { path };
357 db.remove_files();
358 let conn = Connection::open(&db.path).expect("create db file");
359 conn.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY);")
360 .expect("create table");
361 db
362 }
363
364 fn path(&self) -> &Path {
365 &self.path
366 }
367
368 fn remove_files(&self) {
369 let _ = fs_err::remove_file(&self.path);
370 let _ = fs_err::remove_file(self.path.with_extension("sqlite3-wal"));
371 let _ = fs_err::remove_file(self.path.with_extension("sqlite3-shm"));
372 }
373 }
374
375 impl Drop for TempDb {
376 fn drop(&mut self) {
377 self.remove_files();
378 }
379 }
380
381 fn open_db(temp: &TempDb) -> Database {
382 Database::new_with_pool_size(temp.path(), NonZeroUsize::new(4).unwrap()).unwrap()
383 }
384
385 async fn count_items(db: &Database) -> i64 {
386 db.read::<_, DatabaseError, _>("count", |r| {
387 Ok(r.query("SELECT COUNT(*) FROM items", &[], |row| row.get::<i64>(0))?
388 .into_iter()
389 .next()
390 .unwrap_or(0))
391 })
392 .await
393 .unwrap()
394 }
395
396 async fn insert_committed(db: &Database, id: i64) {
397 let tx = db.begin_write().await.unwrap();
398 tx.run::<_, DatabaseError, _>("insert", move |w| {
399 w.execute("INSERT INTO items (id) VALUES (?1)", &[&id])?;
400 Ok(())
401 })
402 .await
403 .unwrap();
404 tx.commit().await.unwrap();
405 }
406
407 #[tokio::test]
408 async fn held_write_transaction_commits_across_awaits() {
409 let temp = TempDb::new("commit");
410 let db = open_db(&temp);
411
412 let tx = db.begin_write().await.unwrap();
413 tx.run::<_, DatabaseError, _>("insert-1", |w| {
414 w.execute("INSERT INTO items (id) VALUES (?1)", &[&1i64])?;
415 Ok(())
416 })
417 .await
418 .unwrap();
419
420 tokio::task::yield_now().await;
422
423 tx.run::<_, DatabaseError, _>("insert-2", |w| {
424 w.execute("INSERT INTO items (id) VALUES (?1)", &[&2i64])?;
425 Ok(())
426 })
427 .await
428 .unwrap();
429
430 tx.commit().await.unwrap();
431
432 assert_eq!(count_items(&db).await, 2);
433 }
434
435 #[tokio::test]
436 async fn dropped_write_transaction_rolls_back() {
437 let temp = TempDb::new("rollback");
438 let db = open_db(&temp);
439
440 {
441 let tx = db.begin_write().await.unwrap();
442 tx.run::<_, DatabaseError, _>("insert", |w| {
443 w.execute("INSERT INTO items (id) VALUES (?1)", &[&1i64])?;
444 Ok(())
445 })
446 .await
447 .unwrap();
448 }
450
451 insert_committed(&db, 2).await;
455 assert_eq!(count_items(&db).await, 1);
456 }
457
458 #[tokio::test]
459 async fn reads_proceed_while_write_transaction_is_held() {
460 let temp = TempDb::new("concurrent");
461 let db = open_db(&temp);
462 insert_committed(&db, 1).await;
463
464 let tx = db.begin_write().await.unwrap();
466 tx.run::<_, DatabaseError, _>("insert-uncommitted", |w| {
467 w.execute("INSERT INTO items (id) VALUES (?1)", &[&2i64])?;
468 Ok(())
469 })
470 .await
471 .unwrap();
472
473 assert_eq!(count_items(&db).await, 1);
476
477 tx.commit().await.unwrap();
478 assert_eq!(count_items(&db).await, 2);
479 }
480
481 #[tokio::test]
482 async fn reader_connections_are_query_only() {
483 let temp = TempDb::new("query_only");
484 let db = open_db(&temp);
485
486 let query_only = db
487 .read::<_, DatabaseError, _>("pragma", |r| {
488 Ok(r.query("PRAGMA query_only", &[], |row| row.get::<i64>(0))?
489 .into_iter()
490 .next()
491 .unwrap_or(0))
492 })
493 .await
494 .unwrap();
495 assert_eq!(query_only, 1, "reader connections must be query_only");
496
497 let result = db
499 .read::<(), DatabaseError, _>("rejected-write", |r| {
500 r.query("INSERT INTO items (id) VALUES (99)", &[], |_| Ok(()))?;
501 Ok(())
502 })
503 .await;
504 assert!(result.is_err(), "writes on a reader connection must fail");
505 }
506}