Skip to main content

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}