Skip to main content

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}