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}