rust_query/
token.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
use std::cell::Cell;

use rusqlite::Connection;

use crate::{Database, Transaction, TransactionMut};

/// The primary interface to the database.
///
/// Only one [LocalClient] can exist in each thread and transactions need to mutably borrow a [LocalClient].
/// This makes it impossible to have access to two transactions from one thread.
///
/// The only way to have concurrent read transactions is to have them on different threads.
/// Write transactions never run in parallell with each other, but they do run in parallel with read transactions.
pub struct LocalClient {
    _p: std::marker::PhantomData<*const ()>,
    pub(crate) conn: Option<Connection>,
}

impl LocalClient {
    /// Create a [Transaction]. This operation always completes immediately as it does not need to wait on other transactions.
    ///
    /// This function will panic if the schema was modified compared to when the [Database] value
    /// was created. This can happen for example by running another instance of your program with
    /// additional migrations.
    pub fn transaction<S>(&mut self, db: &Database<S>) -> Transaction<S> {
        use r2d2::ManageConnection;
        // TODO: could check here if the existing connection is good to use.
        let conn = self.conn.insert(db.manager.connect().unwrap());
        let txn = conn.transaction().unwrap();
        Transaction::new_checked(txn, db.schema_version)
    }

    /// Create a [TransactionMut].
    /// This operation needs to wait for all other [TransactionMut]s for this database to be finished.
    ///
    /// The implementation uses the [unlock_notify](https://sqlite.org/unlock_notify.html) feature of sqlite.
    /// This makes it work across processes.
    ///
    /// Note: you can create a deadlock if you are holding on to another lock while trying to
    /// get a mutable transaction!
    ///
    /// This function will panic if the schema was modified compared to when the [Database] value
    /// was created. This can happen for example by running another instance of your program with
    /// additional migrations.
    pub fn transaction_mut<S>(&mut self, db: &Database<S>) -> TransactionMut<S> {
        use r2d2::ManageConnection;
        // TODO: could check here if the existing connection is good to use.
        // TODO: make sure that when reusing a connection, the foreign keys are checked (migration doesn't)
        // .pragma_update(None, "foreign_keys", "ON").unwrap();
        let conn = self.conn.insert(db.manager.connect().unwrap());
        let txn = conn
            .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
            .unwrap();
        TransactionMut {
            inner: Transaction::new_checked(txn, db.schema_version),
        }
    }
}

thread_local! {
    static EXISTS: Cell<bool> = const { Cell::new(true) };
}

impl LocalClient {
    fn new() -> Self {
        LocalClient {
            _p: std::marker::PhantomData,
            conn: None,
        }
    }

    /// Create a [LocalClient] if it was not created yet on this thread.
    ///
    /// Async tasks often share their thread and can thus not use this method.
    /// Instead you should use your equivalent of `spawn_blocking` or `block_in_place`.
    /// These functions guarantee that you have a unique thread and thus allow [LocalClient::try_new].
    ///
    /// Note that using `spawn_blocking` for sqlite is actually a good practice.
    /// Sqlite queries can be expensive, it might need to read from disk which is slow.
    /// Doing so on all async runtime threads would prevent other tasks from executing.
    pub fn try_new() -> Option<Self> {
        EXISTS.replace(false).then(LocalClient::new)
    }
}

impl Drop for LocalClient {
    /// Dropping a [LocalClient] allows retrieving it with [LocalClient::try_new] again.
    fn drop(&mut self) {
        EXISTS.set(true)
    }
}