p2panda_store/sqlite.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! SQLite database implementation with associated utility functions.
4use std::sync::Arc;
5
6use p2panda_core::cbor::EncodeError;
7use sqlx::migrate::{MigrateDatabase, Migrator};
8use sqlx::sqlite::SqlitePoolOptions;
9use sqlx::{Sqlite, migrate};
10use thiserror::Error;
11use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore};
12
13/// Creates the SQLite database if it doesn't already exist.
14pub async fn create_database(url: &str) -> Result<(), SqliteError> {
15 if !Sqlite::database_exists(url).await? {
16 Sqlite::create_database(url).await?
17 }
18 Ok(())
19}
20
21/// Drops the SQLite database if it exists.
22pub async fn drop_database(url: &str) -> Result<(), SqliteError> {
23 if Sqlite::database_exists(url).await? {
24 Sqlite::drop_database(url).await?
25 }
26 Ok(())
27}
28
29/// Creates the SQLite connection pool.
30pub async fn connection_pool(
31 url: &str,
32 max_connections: u32,
33) -> Result<sqlx::SqlitePool, SqliteError> {
34 let pool: sqlx::SqlitePool = SqlitePoolOptions::new()
35 .max_connections(max_connections)
36 .connect(url)
37 .await?;
38 Ok(pool)
39}
40
41/// Gets migrations from folder without running them.
42pub fn migrations() -> Migrator {
43 migrate!()
44}
45
46/// Runs any pending database migrations from inside the application.
47pub async fn run_pending_migrations(pool: &sqlx::SqlitePool) -> Result<(), SqliteError> {
48 migrations().run(pool).await?;
49 Ok(())
50}
51
52/// Builder for `SqliteStore`.
53///
54/// To create the database call `SqliteStoreBuilder::build()`.
55///
56/// By default, the builder configures an in-memory database with a maximum number of 16
57/// connections. The database is created if it doesn't already exist and migrations are
58/// automatically run on start-up.
59pub struct SqliteStoreBuilder {
60 url: String,
61 max_connections: u32,
62 run_migrations: bool,
63 create_database: bool,
64}
65
66impl Default for SqliteStoreBuilder {
67 fn default() -> Self {
68 Self {
69 url: "sqlite::memory:".into(),
70 max_connections: 16,
71 create_database: true,
72 run_migrations: true,
73 }
74 }
75}
76
77impl SqliteStoreBuilder {
78 /// Creates a new `SqliteStoreBuilder` using default configuration values.
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 /// Assigns a randomly-generated in-memory database URL with private cache.
84 #[cfg(any(test, feature = "test_utils"))]
85 pub fn random_memory_url(mut self) -> Self {
86 // Combining Rust tests with in-memory databases can lead to unsound behaviour, this
87 // "workaround" assigns every temporary database a different, random name and keeps them
88 // isolated from other tests.
89 //
90 // See related issue: https://github.com/launchbadge/sqlx/issues/2510
91 self.url = format!(
92 "sqlite://dbmem{}?mode=memory&cache=private",
93 rand::random::<u32>()
94 );
95 self
96 }
97
98 /// Sets the database URL.
99 ///
100 /// If left unset, the database will use an ephemeral in-memory URL.
101 pub fn database_url(mut self, url: &str) -> Self {
102 self.url = url.to_string();
103 self
104 }
105
106 /// Sets the maximum number of connections to be maintained by the database pool.
107 ///
108 /// If left unset, a maximum of 16 connections will be maintained.
109 pub fn max_connections(mut self, max_connections: u32) -> Self {
110 self.max_connections = max_connections;
111 self
112 }
113
114 /// Creates the database if it doesn't already exist.
115 ///
116 /// If left unset, the database will be created by default.
117 pub fn create_database(mut self, create_database: bool) -> Self {
118 self.create_database = create_database;
119 self
120 }
121
122 /// Sets whether pending migrations should be applied when the database is built.
123 ///
124 /// If left unset, the database will apply any pending migrations.
125 pub fn run_default_migrations(mut self, run_migrations: bool) -> Self {
126 self.run_migrations = run_migrations;
127 self
128 }
129
130 /// Builds the `SqliteStore`.
131 pub async fn build(self) -> Result<SqliteStore, SqliteError> {
132 if self.create_database {
133 create_database(&self.url).await?;
134 }
135
136 let pool: sqlx::SqlitePool = SqlitePoolOptions::new()
137 .max_connections(self.max_connections)
138 .connect(&self.url)
139 .await?;
140
141 if self.run_migrations {
142 run_pending_migrations(&pool).await?;
143 }
144
145 Ok(SqliteStore::new(pool))
146 }
147}
148
149/// An in-progress database transaction.
150pub type Transaction<'a> = sqlx::Transaction<'a, Sqlite>;
151
152/// Sqlite connection pool.
153pub type SqlitePool = sqlx::SqlitePool;
154
155/// SQLite database with connection pool and transaction provider.
156///
157/// This struct can be cloned and used in multiple places in the application. Every cloned instance
158/// will re-use the same connection pool and have access to the same transaction instance if one
159/// was started. To guard against sharing transactions unknowingly across unrelated database
160/// queries, a concept of a `TransactionPermit` was introduced which does not protect from misuse
161/// but helps to make "holding" a transaction explicit.
162///
163/// Please note that SQLite strictly serializes transactions with _writes_ and will block any
164/// parallel attempt to begin another one. Processes starting a transaction will acquire a
165/// `TransactionPermit` and keep it until the transaction was committed or rolled back. If the
166/// query only involves _reads_ it is recommended to not use transactions and use the `execute`
167/// method directly as acquiring transactions will potentially block other processes to do work.
168///
169/// ## Design decisions
170///
171/// This storage API design was chosen to make the dynamics of the underlying SQLite database
172/// explicit to avoid potentially introducing subtle bugs. Internally any process can access the
173/// transaction object to do writes and (uncommitted) reads (see "Transaction I" in diagram). Care
174/// is required when designing systems like that as it's still possible to allow concurrent
175/// processes to read and write within the same transaction (for example one process could roll
176/// back the transaction while the other one assumed it will be committed). Usually developers want
177/// to design _writes_ to the database within a transaction if they need consistency and atomicity
178/// guarantees. "Unrelated" queries _can_ be "pooled" in one transaction (for performance reasons
179/// for example) if consistency is guaranteed by all involved processes and the underlying
180/// data-model (see "Transaction II" in diagram).
181///
182/// ```text
183/// Transaction I:
184/// begin ---------------------> commit
185///
186/// Process I:
187/// --> write --> read -->
188///
189/// Transaction II:
190/// begin ----------------------> commit
191///
192/// Process II:
193/// --> write --> write -->
194///
195/// Process III:
196/// --> read --> write --->
197/// ```
198///
199/// Another design decision is to not expose transactions to the high-level storage APIs (similar
200/// to the "Repository Pattern"). Users of the storage methods like `get_operation` (in
201/// `OperationStore`) etc. do _not_ need to explicity deal with transaction objects, as this is
202/// handled internally now. Like this it is possible to separate the "logic" from the "storage"
203/// layer and keep the code clean.
204#[derive(Clone, Debug)]
205pub struct SqliteStore {
206 tx: Arc<Mutex<Option<Transaction<'static>>>>,
207 pub(crate) pool: sqlx::SqlitePool,
208 semaphore: Arc<Semaphore>,
209}
210
211impl SqliteStore {
212 /// Creates a new `SqliteStore` using the provided connection pool.
213 pub(crate) fn new(pool: sqlx::SqlitePool) -> Self {
214 Self {
215 tx: Arc::default(),
216 pool,
217 // SQLite only ever allows _one_ transaction at a time. This might be a repetition of
218 // what sqlx and SQLite do under the hood, but we want to make this behaviour explicit
219 // right from the beginning with this semaphore.
220 semaphore: Arc::new(Semaphore::new(1)),
221 }
222 }
223
224 /// Creates a new `SqliteStore` using the provided connection pool.
225 pub fn from_pool(pool: sqlx::SqlitePool) -> Self {
226 Self::new(pool)
227 }
228
229 /// Builds an in-memory SQLite database with a randomised name for testing purposes.
230 #[cfg(any(test, feature = "test_utils"))]
231 pub async fn temporary() -> Self {
232 SqliteStoreBuilder::new()
233 .random_memory_url()
234 .max_connections(1)
235 .build()
236 .await
237 .expect("migrations succeeded")
238 }
239
240 /// Executes a SQL query within a transaction.
241 ///
242 /// This method will return an error when no transaction is currently given. Make sure to call
243 /// `begin` before.
244 ///
245 /// If the query fails the user probably wants to roll back the transaction and free the
246 /// permit. This is _not_ handled automatically.
247 pub async fn tx<F, R>(&self, f: F) -> Result<R, SqliteError>
248 where
249 F: AsyncFnOnce(&mut Transaction) -> Result<R, SqliteError>,
250 {
251 let mut tx_ref = self.tx.lock().await;
252 let tx = tx_ref.as_mut().ok_or(SqliteError::TransactionMissing)?;
253
254 f(tx).await
255 }
256
257 /// Executes a SQL query directly.
258 pub async fn execute<F, R>(&self, f: F) -> Result<R, SqliteError>
259 where
260 F: AsyncFnOnce(&sqlx::SqlitePool) -> Result<R, SqliteError>,
261 {
262 f(&self.pool).await
263 }
264}
265
266impl crate::traits::Transaction for SqliteStore {
267 type Error = SqliteError;
268
269 type Permit = TransactionPermit;
270
271 /// Begins a transaction.
272 ///
273 /// Transactions are strictly serialized, this is expressed in form of a `TransactionPermit`
274 /// processes need to hold when acquiring access to a new transaction. Any concurrent process
275 /// calling it will await here if there's already another process holding a permit, this will
276 /// potentially "slow down" work and should be carefully used.
277 ///
278 /// Any process with a transaction can now start using the `tx` method to execute writes within
279 /// this transaction or perform uncommitted "dirty" reads on it.
280 ///
281 /// It is usually not necessary to acquire a transaction when the logic only requires committed
282 /// _reads_ to the database. Use `execute` instead.
283 async fn begin(&self) -> Result<TransactionPermit, SqliteError> {
284 // Acquire a permit from the semaphore, it will await if currently another process has the
285 // permit. Here we enforce strict serialization of transactions (similar to what SQLite
286 // does under the hood).
287 let permit = self
288 .semaphore
289 .clone()
290 .acquire_owned()
291 .await
292 .expect("if semaphore is closed then the whole struct is gone as well");
293
294 // Access the transaction object which we've placed behind a Mutex. This lock follows a
295 // different logic and only makes sure that mutable access to it is exclusive _within_ a
296 // process "holding" the transaction permit.
297 let mut tx_ref = self.tx.lock().await;
298 assert!(
299 tx_ref.is_none(),
300 "can't have an already existing transaction after an just-acquired permit"
301 );
302 let tx = self.pool.begin().await?;
303 tx_ref.replace(tx);
304
305 Ok(TransactionPermit::new(permit, self.tx.clone()))
306 }
307
308 /// Rolls back the transaction and with that all uncommitted changes.
309 ///
310 /// This takes the permit and frees it after the rollback has finished. Other processes can now
311 /// begin new transactions.
312 async fn rollback(&self, permit: TransactionPermit) -> Result<(), SqliteError> {
313 let Some(tx) = self.tx.lock().await.take() else {
314 panic!("can't have no transaction without dropping permit first")
315 };
316
317 let result = tx.rollback().await.map_err(SqliteError::Sqlite);
318
319 // Always drop the permit, both on successful rollback and error. This will allow other
320 // processes now to begin a new transaction and acquire the permit.
321 permit.mark_committed_and_drop();
322
323 result
324 }
325
326 /// Commits the transaction.
327 ///
328 /// This takes the permit and frees it after the commit has finished. Other processes can now
329 /// begin new transactions.
330 async fn commit(&self, permit: TransactionPermit) -> Result<(), SqliteError> {
331 let Some(tx) = self.tx.lock().await.take() else {
332 panic!("can't have no transaction without dropping permit first")
333 };
334
335 let result = tx.commit().await.map_err(SqliteError::Sqlite);
336
337 // Always drop the permit, both on successful commit and error. This will allow other
338 // processes now to begin a new transaction and acquire the permit.
339 permit.mark_committed_and_drop();
340
341 result
342 }
343}
344
345/// Locked context marking the lifetime of a single transaction.
346pub struct TransactionPermit {
347 permit: Arc<OwnedSemaphorePermit>,
348 tx: Arc<Mutex<Option<Transaction<'static>>>>,
349 committed: bool,
350}
351
352impl TransactionPermit {
353 /// Creates a new `TransactionPermit` using the given permit and transaction.
354 pub(super) fn new(
355 permit: OwnedSemaphorePermit,
356 tx: Arc<Mutex<Option<Transaction<'static>>>>,
357 ) -> Self {
358 Self {
359 permit: Arc::new(permit),
360 tx,
361 committed: false,
362 }
363 }
364
365 /// Marks the transaction as committed and drops the permit.
366 ///
367 /// In the case that the permit was never used, whether due to an early return or error, the
368 /// transaction is automatically rolled-back to prevent corrupted state.
369 pub(super) fn mark_committed_and_drop(mut self) {
370 self.committed = true;
371 drop(self)
372 }
373}
374
375impl Drop for TransactionPermit {
376 fn drop(&mut self) {
377 // If the permit was never used (due to an early return / error / etc.) we automatically
378 // roll-back the transaction.
379 if !self.committed {
380 let permit = self.permit.clone();
381 let tx = self.tx.clone();
382
383 tokio::spawn(async move {
384 if let Some(tx) = tx.lock().await.take() {
385 let _ = tx.rollback().await;
386 }
387
388 drop(permit); // Semaphore released only after rollback completes.
389 });
390 }
391 }
392}
393
394/// Error when interacting with a SQLite store implementation.
395#[derive(Debug, Error)]
396pub enum SqliteError {
397 /// This is a critical error as it indicates that something is wrong with the usage of this
398 /// API: Queries using transactions can only ever occur if a transaction was started _before_.
399 #[error("tried to interact with inexistant transaction")]
400 TransactionMissing,
401
402 /// SQLite database and connection error.
403 #[error(transparent)]
404 Sqlite(#[from] sqlx::Error),
405
406 /// SQL table schema migration error.
407 #[error(transparent)]
408 Migrate(#[from] sqlx::migrate::MigrateError),
409
410 /// An I/O error occurred while encoding bytes before storing them into the database. This is a
411 /// critical error.
412 #[error("failed encoding '{0}' value before storing to database: {1}")]
413 Encode(String, EncodeError),
414
415 /// Invalid, corrupted data was found in the database. This is a critical error.
416 #[error("could not decode corrupted '{0}' value from database: {1}")]
417 Decode(String, DecodeError),
418}
419
420/// Error decoding value retrieved from a store.
421#[derive(Debug, Error)]
422pub enum DecodeError {
423 #[error(transparent)]
424 DecodeCbor(#[from] p2panda_core::cbor::DecodeError),
425
426 #[error(transparent)]
427 Hash(#[from] p2panda_core::hash::HashError),
428
429 #[error(transparent)]
430 Topic(#[from] p2panda_core::topic::TopicError),
431
432 #[error("parsing from string failed")]
433 FromStr,
434}
435
436#[cfg(test)]
437mod tests {
438 use std::task::Poll;
439
440 use futures_test::task::noop_context;
441 use sqlx::{Executor, query, query_as, query_scalar};
442 use tokio::pin;
443
444 use crate::sqlite::{SqliteError, SqliteStore};
445 use crate::traits::Transaction;
446
447 #[tokio::test]
448 async fn transaction_provider() {
449 let pool = SqliteStore::temporary().await;
450
451 // Executing with an in-existant transaction should throw error.
452 assert!(matches!(
453 pool.tx(async |_| Ok(())).await,
454 Err(SqliteError::TransactionMissing)
455 ));
456
457 // Starting a new transaction should work.
458 let permit = pool.begin().await.expect("no error");
459
460 // .. attempting to start a second one should make us wait.
461 assert!(matches!(
462 {
463 let fut = pool.begin();
464 let mut cx = noop_context();
465 pin!(fut);
466 fut.poll(&mut cx)
467 },
468 Poll::Pending
469 ));
470
471 // Using the transaction should work without failure.
472 assert!(pool.tx(async |_| Ok(())).await.is_ok());
473
474 // Committing should work as well.
475 assert!(pool.commit(permit).await.is_ok());
476
477 // .. and now running a transaction should fail.
478 assert!(matches!(
479 pool.tx(async |_| Ok(())).await,
480 Err(SqliteError::TransactionMissing)
481 ));
482 }
483
484 #[tokio::test]
485 async fn early_permit_drop_causing_rollback() {
486 let pool = SqliteStore::temporary().await;
487
488 // Create test-table schema.
489 pool.execute(async |pool| {
490 pool.execute("CREATE TABLE test(x INTEGER)").await?;
491 Ok(())
492 })
493 .await
494 .unwrap();
495
496 let permit = pool.begin().await.unwrap();
497
498 pool.tx(async |tx| {
499 query("INSERT INTO test (x) VALUES (10)")
500 .execute(&mut **tx)
501 .await?;
502 Ok(())
503 })
504 .await
505 .unwrap();
506
507 // Permit was dropped prematurely without committing.
508 drop(permit);
509
510 // It is okay to start another permit.
511 assert!(pool.begin().await.is_ok());
512
513 // The data was not written as the transaction got rolled back.
514 let count: i64 = pool
515 .execute(async |pool| {
516 query_scalar("SELECT COUNT(*) FROM test")
517 .fetch_one(pool)
518 .await
519 .map_err(SqliteError::Sqlite)
520 })
521 .await
522 .unwrap();
523 assert_eq!(count, 0);
524 }
525
526 #[tokio::test]
527 async fn serialized_transactions() {
528 let pool_1 = SqliteStore::temporary().await;
529
530 let pool_2 = pool_1.clone();
531
532 // Create test-table schema.
533 pool_1
534 .execute(async |pool| {
535 pool.execute("CREATE TABLE test(x INTEGER)").await?;
536 Ok(())
537 })
538 .await
539 .unwrap();
540
541 // 1. Pool 1 acquires the permit to run a transaction.
542 let permit_1 = pool_1.begin().await.unwrap();
543
544 // .. parallely Pool 2 also tries to do some work.
545 let handle = tokio::spawn(async move {
546 // Try to acquire a permit, this will "block" for now as pool 1 already is doing
547 // something and we need to wait.
548 let permit_2 = pool_2.begin().await.unwrap();
549
550 // 5. We should see now the previously change made by pool 1.
551 let result = pool_2
552 .tx(async |tx| {
553 let row: (i64,) = query_as("SELECT x FROM test").fetch_one(&mut **tx).await?;
554 Ok(row.0)
555 })
556 .await
557 .unwrap();
558 assert_eq!(result, 5);
559
560 // 6. Change the value to something else.
561 pool_2
562 .tx(async |tx| {
563 query("INSERT INTO test (x) VALUES (10)")
564 .execute(&mut **tx)
565 .await?;
566 Ok(())
567 })
568 .await
569 .unwrap();
570
571 // 7. .. but abort the transaction and roll back.
572 pool_2.rollback(permit_2).await.unwrap();
573
574 // The value should still be the same as before.
575 let result = pool_2
576 .execute(async |pool| {
577 let row: (i64,) = query_as("SELECT x FROM test").fetch_one(pool).await?;
578 Ok(row.0)
579 })
580 .await
581 .unwrap();
582 assert_eq!(result, 5);
583 });
584
585 // 2. Pool 1 changes the value.
586 pool_1
587 .tx(async |tx| {
588 query("INSERT INTO test (x) VALUES (5)")
589 .execute(&mut **tx)
590 .await?;
591 Ok(())
592 })
593 .await
594 .unwrap();
595
596 // 3. Result is already 5 during "dirty read".
597 let result = pool_1
598 .tx(async |tx| {
599 let row: (i64,) = query_as("SELECT x FROM test").fetch_one(&mut **tx).await?;
600 Ok(row.0)
601 })
602 .await
603 .unwrap();
604 assert_eq!(result, 5);
605
606 // 4. Commit the change to database and free permit. This will allow now pool_2 to read the
607 // changed value.
608 pool_1.commit(permit_1).await.unwrap();
609
610 // Result is still 5 after commit.
611 let result = pool_1
612 .execute(async |pool| {
613 let row: (i64,) = query_as("SELECT x FROM test").fetch_one(pool).await?;
614 Ok(row.0)
615 })
616 .await
617 .unwrap();
618 assert_eq!(result, 5);
619
620 // Make sure we give pool 2 the time it needs to finish.
621 handle.await.unwrap();
622 }
623}