dittolive_ditto/store/transactions/api.rs
1use crate::dql::QueryResult;
2
3use_prelude!();
4
5impl Store {
6 /// Executes multiple DQL queries within a single atomic transaction.
7 ///
8 /// ```rust,no_run
9 /// # use dittolive_ditto::prelude::*;
10 /// # use serde_json::json;
11 /// # #[tokio::main]
12 /// # async fn main() {
13 /// # let ditto: Ditto = todo!();
14 /// ditto.store().transaction(async |txn| {
15 /// txn.execute((
16 /// "INSERT INTO users DOCUMENTS (:doc)",
17 /// json!({"name": "alice"}),
18 /// )).await?;
19 ///
20 /// txn.execute((
21 /// "INSERT INTO users DOCUMENTS (:doc)",
22 /// json!({"name": "bob"}),
23 /// )).await?;
24 ///
25 /// Ok::<_, DittoError>(TransactionCompletionAction::Commit)
26 /// }).await.unwrap();
27 /// # }
28 /// ```
29 /// The closure returns a [`Result<T, E>`][core::result::Result], and in the success case, this
30 /// value will be returned from the call to
31 /// [`ditto.store().transaction()`][Store::transaction]. However, if the value is a
32 /// [`TransactionCompletionAction`] (i.e. if `T = TransactionCompletionAction`), then this
33 /// value is used to determine the behaviour of the transaction:
34 /// - if the value is [`TransactionCompletionAction::Commit`], the transaction is committed
35 /// - if the value is [`TransactionCompletionAction::Rollback`], the transaction is rolled back
36 /// - if the value is any other type, the transaction is implicitly committed
37 ///
38 /// See the "errors" section below for more information on the error case.
39 ///
40 /// This ensures that either all statements are executed successfully, or none are executed at
41 /// all, providing strong consistency guarantees. Certain mesh configurations may impose
42 /// limitations on these guarantees. For more details, refer to the [Ditto
43 /// documentation](https://ditto.com/link/sdk-latest-crud-transactions). Transactions are
44 /// initiated as read-write transactions by default, and only a single read-write transaction
45 /// is being executed at any given time. Any other read-write transaction started concurrently
46 /// will wait until the current transaction has been committed or rolled back. Therefore, it is
47 /// crucial to make sure a transaction finishes as early as possible so other read-write
48 /// transactions aren't blocked for a long time.
49 ///
50 /// A transaction can also be configured to be read-only, or given an option debugging `hint`
51 /// via [`ditto.store.transaction_with_options`][Store::transaction_with_options]. See the docs
52 /// for more information.
53 ///
54 /// # Errors
55 ///
56 /// See the [module-level docs][module] for more details on error types in transactions.
57 ///
58 /// If errors occur in an [`execute()`][Transaction::execute] call within a transaction block,
59 /// this error can be handled like a normal [`Result`], and the transaction will continue
60 /// without being rolled back. If the transaction callback itself returns an [`Err`], the
61 /// transaction is implicitly rolled back and the error is propagated to the called.
62 ///
63 /// For a complete guide on transactions, please refer to the [Ditto
64 /// documentation](https://ditto.com/link/sdk-latest-crud-transactions).
65 ///
66 /// See also - [`Transaction`]
67 ///
68 /// [module]: crate::store::transactions
69 pub async fn transaction<T, E>(
70 &self,
71 scope: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
72 ) -> Result<T, E>
73 where
74 T: core::any::Any,
75 E: From<DittoError>,
76 {
77 let options = CreateTransactionOptions::default();
78 self.transaction_with_options(options, scope).await
79 }
80
81 /// Create a transaction with the provided options.
82 ///
83 /// ```rust,no_run
84 /// # use dittolive_ditto::prelude::*;
85 /// # use serde_json::json;
86 /// # #[tokio::main]
87 /// # async fn main() {
88 /// # let ditto: Ditto = todo!();
89 /// let mut opts = CreateTransactionOptions::new();
90 /// opts.hint = Some("debug transaction name");
91 /// opts.is_read_only = false;
92 ///
93 /// ditto.store().transaction_with_options(opts, async |txn| {
94 /// txn.execute((
95 /// "INSERT INTO users DOCUMENTS (:doc)",
96 /// json!({"name": "alice"}),
97 /// )).await?;
98 ///
99 /// txn.execute((
100 /// "INSERT INTO users DOCUMENTS (:doc)",
101 /// json!({"name": "bob"}),
102 /// )).await?;
103 ///
104 /// Ok::<_, DittoError>(TransactionCompletionAction::Commit)
105 /// }).await.unwrap();
106 /// # }
107 /// ```
108 /// If [`is_read_only`][CreateTransactionOptions::is_read_only] is `true`, mutating DQL queries
109 /// will error, even if no actual mutation would have occurred.
110 ///
111 /// See [`ditto.store().transaction()`][Store::transaction] for more documentation on the
112 /// behaviour of transactions in general.
113 pub async fn transaction_with_options<T, E>(
114 &self,
115 options: CreateTransactionOptions<'_>,
116 scope: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
117 ) -> Result<T, E>
118 where
119 T: core::any::Any,
120 E: From<DittoError>,
121 {
122 let hint = options.hint.map(char_p::new);
123
124 let options = ffi_sdk::BeginTransactionOptions {
125 hint: hint.as_ref().map(|s| s.as_ref()),
126 is_read_only: options.is_read_only,
127 };
128
129 self._transaction_with_options(options, scope).await
130 }
131}
132
133/// Options for customizing a transaction. Used with [`Store::transaction_with_options`].
134///
135/// ```rust,no_run
136/// # use dittolive_ditto::prelude::*;
137/// # #[tokio::main]
138/// # async fn main() {
139/// # let ditto: Ditto = todo!();
140/// let mut options = CreateTransactionOptions::new();
141/// options.hint = Some("my transaction name");
142/// options.is_read_only = true;
143///
144/// ditto
145/// .store()
146/// .transaction_with_options(options, async |txn| {
147/// // do transaction stuff
148/// Ok::<_, DittoError>(TransactionCompletionAction::Commit)
149/// })
150/// .await
151/// .unwrap();
152/// # }
153/// ```
154#[derive(Debug, Clone, PartialEq)]
155#[non_exhaustive]
156pub struct CreateTransactionOptions<'hint> {
157 /// A hint used for debugging and logging
158 pub hint: Option<&'hint str>,
159
160 /// Whether the transaction should be created read-only
161 pub is_read_only: bool,
162}
163
164#[allow(
165 clippy::derivable_impls,
166 reason = "writing it out makes it clearer what the defaults are"
167)]
168impl Default for CreateTransactionOptions<'_> {
169 fn default() -> Self {
170 Self {
171 hint: None,
172 is_read_only: false,
173 }
174 }
175}
176
177impl CreateTransactionOptions<'_> {
178 /// Create a new, default [`CreateTransactionOptions`].
179 pub fn new() -> Self {
180 Self::default()
181 }
182}
183
184/// Encapsulates information about a transaction.
185#[non_exhaustive]
186#[derive(Debug, Clone, PartialEq)]
187pub struct TransactionInfo {
188 /// A globally unique ID of the transaction.
189 pub id: String,
190
191 /// The user hint passed when creating the transaction, useful for debugging
192 /// and testing.
193 pub hint: Option<String>,
194
195 /// Indicates whether mutating DQL statements can be executed in the transaction. Defaults to
196 /// `false`. See [`ditto.store().transaction()`][Store::transaction] for more information.
197 pub is_read_only: bool,
198}
199
200/// Represents an action that completes a transaction, by either committing it or
201/// rolling it back.
202#[derive(Debug, Clone, PartialEq)]
203#[must_use = "Code should handle whether a transaction succeeded"]
204pub enum TransactionCompletionAction {
205 /// Represents the action of committing a transaction.
206 Commit,
207
208 /// Represents the action of rolling back a transaction.
209 Rollback,
210}
211
212/// Represents a transaction in the Ditto store.
213///
214/// A [`Transaction`] is used to group multiple operations into a single atomic unit. This ensures
215/// that either all operations within the transaction are applied, or none of them are, maintaining
216/// the integrity of the data.
217///
218/// Please consult the documentation of [`ditto.store().transaction()`][Store::transaction] or the
219/// [module-level docs][module] for more information on how to create and use transactions. For a
220/// complete guide on transactions, please refer to the [Ditto documentation][docs]
221///
222/// [module]: crate::store::transactions
223/// [docs]: https://ditto.com/link/sdk-latest-crud-transactions
224pub struct Transaction {
225 pub(crate) ptr: repr_c::Box<ffi_sdk::FfiTransaction>,
226}
227
228impl Transaction {
229 /// Information about the current transaction.
230 pub fn info(&self) -> TransactionInfo {
231 self._info()
232 }
233
234 /// Executes a DQL query and returns matching items as a [`QueryResult`].
235 ///
236 /// Note that this method only returns results from the local store without waiting for any
237 /// [`SyncSubscription`][crate::sync::SyncSubscription]s to have caught up with the latest
238 /// changes. Only use this method if your program must proceed with immediate results. Use a
239 /// [`StoreObserver`][crate::store::StoreObserver] (obtained from
240 /// [`ditto.store().register_observer()`][crate::store::Store::register_observer]) to receive
241 /// updates to query results as soon as they have been synced to this peer.
242 pub async fn execute<Q>(&self, query: Q) -> Result<QueryResult, DittoError>
243 where
244 Q: IntoQuery,
245 Q::Args: serde::Serialize,
246 {
247 self._execute(query).await
248 }
249}