rust_query/
client.rs

1use std::cell::Cell;
2
3use rusqlite::Connection;
4
5use crate::{Database, Transaction, TransactionMut};
6
7/// The primary interface to the database.
8///
9/// Only one [LocalClient] can exist in each thread and transactions need to mutably borrow a [LocalClient].
10/// This makes it impossible to have access to two transactions from one thread.
11///
12/// The only way to have concurrent read transactions is to have them on different threads.
13/// Write transactions never run in parallell with each other, but they do run in parallel with read transactions.
14pub struct LocalClient {
15    _p: std::marker::PhantomData<*const ()>,
16    pub(crate) conn: Option<Connection>,
17}
18
19impl LocalClient {
20    /// Create a [Transaction]. This operation always completes immediately as it does not need to wait on other transactions.
21    ///
22    /// This function will panic if the schema was modified compared to when the [Database] value
23    /// was created. This can happen for example by running another instance of your program with
24    /// additional migrations.
25    pub fn transaction<S>(&mut self, db: &Database<S>) -> Transaction<S> {
26        use r2d2::ManageConnection;
27        // TODO: could check here if the existing connection is good to use.
28        let conn = self.conn.insert(db.manager.connect().unwrap());
29        let txn = conn.transaction().unwrap();
30        Transaction::new_checked(txn, db.schema_version)
31    }
32
33    /// Create a [TransactionMut].
34    /// This operation needs to wait for all other [TransactionMut]s for this database to be finished.
35    ///
36    /// The implementation uses the [unlock_notify](https://sqlite.org/unlock_notify.html) feature of sqlite.
37    /// This makes it work across processes.
38    ///
39    /// Note: you can create a deadlock if you are holding on to another lock while trying to
40    /// get a mutable transaction!
41    ///
42    /// This function will panic if the schema was modified compared to when the [Database] value
43    /// was created. This can happen for example by running another instance of your program with
44    /// additional migrations.
45    pub fn transaction_mut<S>(&mut self, db: &Database<S>) -> TransactionMut<S> {
46        use r2d2::ManageConnection;
47        // TODO: could check here if the existing connection is good to use.
48        // TODO: make sure that when reusing a connection, the foreign keys are checked (migration doesn't)
49        // .pragma_update(None, "foreign_keys", "ON").unwrap();
50        let conn = self.conn.insert(db.manager.connect().unwrap());
51        let txn = conn
52            .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
53            .unwrap();
54        TransactionMut {
55            inner: Transaction::new_checked(txn, db.schema_version),
56        }
57    }
58}
59
60thread_local! {
61    static EXISTS: Cell<bool> = const { Cell::new(true) };
62}
63
64impl LocalClient {
65    fn new() -> Self {
66        LocalClient {
67            _p: std::marker::PhantomData,
68            conn: None,
69        }
70    }
71
72    /// Create a [LocalClient] if it was not created yet on this thread.
73    ///
74    /// Async tasks often share their thread and can thus not use this method.
75    /// Instead you should use your equivalent of `spawn_blocking` or `block_in_place`.
76    /// These functions guarantee that you have a unique thread and thus allow [LocalClient::try_new].
77    ///
78    /// Note that using `spawn_blocking` for sqlite is actually a good practice.
79    /// Sqlite queries can be expensive, it might need to read from disk which is slow.
80    /// Doing so on all async runtime threads would prevent other tasks from executing.
81    pub fn try_new() -> Option<Self> {
82        EXISTS.replace(false).then(LocalClient::new)
83    }
84}
85
86impl Drop for LocalClient {
87    /// Dropping a [LocalClient] allows retrieving it with [LocalClient::try_new] again.
88    fn drop(&mut self) {
89        EXISTS.set(true)
90    }
91}