dittolive_ditto/store/
mod.rs

1//! Use [`ditto.store()`] to access the [`Store`] API to read, write, and remove documents.
2//!
3//! The `Store` provides two interfaces for interacting with data: Ditto Query Language
4//! (DQL), and the legacy "Query Builder" API. Where possible, we recommend developing new
5//! functionality using DQL, as we will eventually deprecate Query Builder.
6//!
7//! - [See the `dql` module docs for examples of DQL queries in action][0]
8//!
9//! [`ditto.store()`]: crate::prelude::Ditto::store
10//! [0]: crate::dql
11
12use_prelude!();
13
14use std::{
15    ops::Deref,
16    sync::{
17        atomic::{self, AtomicU64},
18        RwLock, Weak,
19    },
20};
21
22use ffi_sdk::{FfiStoreObserver, FsComponent, WriteStrategyRs};
23
24pub mod attachment;
25mod document_id;
26mod observer;
27pub mod transactions;
28pub use document_id::DocumentId;
29
30#[deprecated(note = "Use `ditto.store().execute_v2(...)` or \
31                     `ditto.store().register_observer_v2(...)` instead")]
32#[allow(deprecated)] // Silence warnings within the module itself
33pub mod query_builder;
34pub use self::observer::{ChangeHandler, ChangeHandlerWithSignalNext, SignalNext, StoreObserver};
35#[doc(hidden)]
36#[allow(deprecated)]
37pub use self::query_builder::{batch, collection, collections, live_query, update};
38#[doc(hidden)]
39pub use crate::dql;
40use crate::{
41    disk_usage::DiskUsage,
42    ditto::DittoFields,
43    error::{DittoError, ErrorKind},
44    utils::{extension_traits::FfiResultIntoRustResult, SetArc},
45};
46
47// Legacy
48#[doc(hidden)]
49#[deprecated]
50#[rustfmt::skip]
51pub use self::attachment::{
52    self as ditto_attachment,
53    fetch_event as ditto_attachment_fetch_event,
54    fetcher as ditto_attachment_fetcher,
55    token as ditto_attachment_token,
56};
57use self::{ditto_attachment_fetcher::DittoAttachmentFetcherV2, dql::query_v2::IntoQuery};
58
59#[doc(hidden)]
60#[deprecated(note = "The experimental `timeseries` feature has been deprecated")]
61#[cfg(feature = "timeseries")]
62pub mod timeseries;
63
64#[doc(hidden)]
65#[allow(deprecated)]
66use collections::pending_collections_operation::PendingCollectionsOperation;
67
68use crate::dql::*;
69
70type CancelToken = u64;
71
72/// Use [`ditto.store()`] to access the [`Store`] API to read, write, and remove documents.
73///
74/// [See the `store` module for guide-level docs and examples][0].
75///
76/// [`ditto.store()`]: crate::prelude::Ditto::store
77/// [0]: crate::store
78#[derive(Clone)]
79pub struct Store {
80    ditto: Arc<ffi_sdk::BoxedDitto>,
81    // FIXME(Daniel): unify this field with `.ditto`
82    weak_ditto_fields: Weak<DittoFields>,
83    disk_usage: Arc<DiskUsage>,
84    attachment_fetchers: Arc<RwLock<HashMap<CancelToken, (bool, DittoAttachmentFetcherV2)>>>,
85}
86
87impl Store {
88    pub(crate) fn new(
89        ditto: Arc<ffi_sdk::BoxedDitto>,
90        weak_ditto_fields: Weak<DittoFields>,
91    ) -> Self {
92        let disk_usage = Arc::new(DiskUsage::new(ditto.retain(), FsComponent::Store));
93        Self {
94            ditto,
95            weak_ditto_fields,
96            disk_usage,
97            attachment_fetchers: <_>::default(),
98        }
99    }
100
101    // Note this method's logic will be moved into the core ditto library
102    // in the future
103    fn validate_collection_name(name: &str) -> Result<(), DittoError> {
104        let mut result = Ok(());
105
106        if name.is_empty() {
107            result = Err(DittoError::new(
108                ErrorKind::InvalidInput,
109                String::from("Collection name can not be empty"),
110            ));
111        }
112
113        if name.split_whitespace().next().is_none() {
114            result = Err(DittoError::new(
115                ErrorKind::InvalidInput,
116                String::from("Collection name can not only contain whitespace"),
117            ));
118        }
119
120        result
121    }
122
123    /// Returns a [`Collection`] with the provided name.
124    /// A collection name is valid if :
125    /// * its length is less than 100
126    /// * it is not empty
127    /// * it does not contain the char '\0'
128    /// * it does not begin with "$TS_"
129    #[doc(hidden)]
130    #[deprecated(note = "Will be removed in SDK 5.0. Use DQL queries instead")]
131    #[allow(deprecated)]
132    pub fn collection(&self, collection_name: &'_ str) -> Result<Collection, DittoError> {
133        Self::validate_collection_name(collection_name)?;
134        let c_name = char_p::new(collection_name);
135        let status = { ffi_sdk::ditto_collection(&*self.ditto, c_name.as_ref()) };
136        if status != 0 {
137            return Err(DittoError::from_ffi(ErrorKind::InvalidInput));
138        }
139        Ok(Collection {
140            ditto: Arc::downgrade(&self.ditto),
141            collection_name: c_name,
142        })
143    }
144
145    /// Returns an object that lets you fetch or observe the collections in the
146    /// store.
147    #[doc(hidden)]
148    #[deprecated(note = "Will be removed in SDK 5.0. Use DQL queries instead")]
149    #[allow(deprecated)]
150    pub fn collections(&self) -> PendingCollectionsOperation<'_> {
151        PendingCollectionsOperation::<'_>::new(Arc::downgrade(&self.ditto))
152    }
153
154    /// Allows you to group multiple operations together that affect multiple
155    /// documents, potentially across multiple collections, without
156    /// auto-committing on each operation.
157    ///
158    /// At the end of the batch of operations, either
159    /// [`batch.commit_changes`](crate::store::batch::ScopedStore::commit_changes)
160    /// or
161    /// [`batch.revert_changes`](crate::store::batch::ScopedStore::revert_changes)
162    /// must be called.
163    ///
164    /// ## Example
165    ///
166    /// ```rust
167    /// # macro_rules! ignore {($($__:tt)*) => ()} ignore! {
168    /// ditto.store().with_batched_write(|batch| {
169    ///     let mut foo_coll = batch.collection("foo");
170    ///     foo_coll.find...().remove();
171    ///     let mut bar_coll = batch.collection("bar");
172    ///     // Expensive multi-mutation op:
173    ///     for _ in 0 .. 10_000 {
174    ///         let doc = ...;
175    ///         bar_coll.insert(doc, None, false);
176    ///     }
177    ///     // At this point, we must say whether we commit or revert
178    ///     // these changes:
179    ///     batch.commit_changes()
180    /// })
181    /// # }
182    /// ```
183    #[doc(hidden)]
184    #[deprecated(note = "Will be removed in SDK 5.0. Use DQL queries instead")]
185    #[allow(deprecated)]
186    pub fn with_batched_write<F>(
187        &self,
188        f: F,
189    ) -> Result<Vec<batch::WriteTransactionResult>, DittoError>
190    where
191        for<'batch> F: FnOnce(batch::ScopedStore<'batch>) -> batch::Action<'batch>,
192    {
193        query_builder::batch::with_batched_write(self, f)
194    }
195
196    /// Returns a list of the names of collections in the local store.
197    #[deprecated(
198        since = "4.14.4",
199        note = "Will be removed in SDK 5.0. Use DQL queries instead"
200    )]
201    pub fn collection_names(&self) -> Result<Vec<String>, DittoError> {
202        let c_collections = { ffi_sdk::ditto_get_collection_names(&*self.ditto).ok()? };
203
204        Ok(c_collections
205            .iter()
206            .map(|x: &char_p::Box| -> String { x.clone().into_string() })
207            .collect())
208    }
209
210    /// Returns a hash representing the current version of the given queries.
211    /// When a document matching such queries gets mutated, the hash will change
212    /// as well.
213    ///
214    /// Please note that the hash depends on how queries are constructed, so you
215    /// should make sure to always compare hashes generated with the same set of
216    /// queries.
217    #[doc(hidden)]
218    #[deprecated(note = "Will be removed in SDK 5.0. Use DQL queries instead")]
219    #[allow(deprecated)]
220    pub fn queries_hash(&self, live_queries: &[LiveQuery]) -> Result<u64, DittoError> {
221        let (coll_names, queries): (Vec<_>, Vec<_>) = live_queries
222            .iter()
223            .map(|lq| (lq.collection_name.as_ref(), lq.query.as_ref()))
224            .unzip();
225
226        {
227            ffi_sdk::ditto_queries_hash(&self.ditto, coll_names[..].into(), queries[..].into()).ok()
228        }
229    }
230
231    /// Returns a sequence of English words representing the current version of
232    /// the given queries. When a document matching such queries gets mutated,
233    /// the words will change as well.
234    ///
235    /// Please note that the resulting sequence of words depends on how queries
236    /// are constructed, so you should make sure to always compare hashes
237    /// generated with the same set of queries.
238    #[doc(hidden)]
239    #[deprecated(note = "Will be removed in SDK 5.0. Use DQL queries instead")]
240    #[allow(deprecated)]
241    pub fn queries_hash_mnemonic(&self, live_queries: &[LiveQuery]) -> Result<String, DittoError> {
242        let (coll_names, queries): (Vec<_>, Vec<_>) = live_queries
243            .iter()
244            .map(|lq| (lq.collection_name.as_ref(), lq.query.as_ref()))
245            .unzip();
246
247        {
248            ffi_sdk::ditto_queries_hash_mnemonic(
249                &self.ditto,
250                coll_names[..].into(),
251                queries[..].into(),
252            )
253            .ok()
254            .map(|c_str| c_str.into_string())
255        }
256    }
257
258    #[doc(hidden)]
259    #[deprecated(note = "All live query webhook related methods have been deprecated")]
260    /// Start all live query webhooks.
261    pub fn start_all_live_query_webhooks(&self) -> Result<(), DittoError> {
262        error!("Store::start_all_live_query_webhooks has been deprecated: it is now a no-op");
263        Err(DittoError::from_str(
264            ErrorKind::Deprecation,
265            "Live query webhooks are deprecated",
266        ))
267    }
268
269    #[doc(hidden)]
270    #[deprecated(note = "All live query webhook related methods have been deprecated")]
271    /// Start a live query webhooks by its id.
272    pub fn start_live_query_webhook_by_id(&self, _doc_id: DocumentId) -> Result<(), DittoError> {
273        error!("Store::start_live_query_webhook_by_id has been deprecated: it is now a no-op");
274        Err(DittoError::from_str(
275            ErrorKind::Deprecation,
276            "Live query webhooks are deprecated",
277        ))
278    }
279
280    #[doc(hidden)]
281    #[deprecated(note = "All live query webhook related methods have been deprecated")]
282    /// Register a new live query webhook
283    pub fn register_live_query_webhook(
284        &self,
285        _collection_name: &str,
286        _query: &str,
287        _url: &str,
288    ) -> Result<DocumentId, DittoError> {
289        error!("Store::register_live_query_webhook has been deprecated: it is now a no-op");
290        Err(DittoError::from_str(
291            ErrorKind::Deprecation,
292            "Live query webhooks are deprecated",
293        ))
294    }
295
296    #[doc(hidden)]
297    #[deprecated(note = "All live query webhook related methods have been deprecated")]
298    /// Generate a new API secret for live query webhook
299    pub fn live_query_webhook_generate_new_api_secret(&self) -> Result<(), DittoError> {
300        error!(
301            "Store::live_query_webhook_generate_new_api_secret has been deprecated: it is now a \
302             no-op"
303        );
304        Err(DittoError::from_str(
305            ErrorKind::Deprecation,
306            "Live query webhooks are deprecated",
307        ))
308    }
309
310    #[doc(hidden)]
311    #[deprecated(note = "The experimental `timeseries` feature has been deprecated")]
312    #[allow(deprecated)]
313    #[cfg(feature = "timeseries")]
314    /// Returns a [`TimeSeries`] with the provided name.
315    pub fn timeseries(&self, ts_name: &'_ str) -> Result<TimeSeries, DittoError> {
316        Self::validate_collection_name(ts_name)?;
317        let c_name = char_p::new(ts_name);
318        Ok(TimeSeries {
319            ditto: self.ditto.retain(),
320            ts_name: c_name,
321        })
322    }
323
324    /// Return a [`DiskUsage`] to monitor the disk usage of the
325    /// [`Store`].
326    #[doc(hidden)]
327    #[deprecated(note = "Use `ditto.disk_usage()` instead")]
328    pub fn disk_usage(&self) -> &DiskUsage {
329        &self.disk_usage
330    }
331
332    /// Deprecated in favour of [`ditto.store().register_observer_with_signal_next_v2()`][0]
333    ///
334    /// [0]: Store::register_observer_with_signal_next_v2
335    #[deprecated(note = "Use `ditto.store().register_observer_v2()` instead")]
336    #[allow(deprecated)]
337    #[doc(hidden)]
338    pub fn register_observer<Q, F>(
339        &self,
340        query: Q,
341        query_args: Option<QueryArguments>,
342        on_change: F,
343    ) -> Result<Arc<StoreObserver>, DittoError>
344    where
345        Q: TryInto<Query, Error = DittoError>,
346        F: ChangeHandler,
347    {
348        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
349
350        let query = query.try_into()?;
351        let query_args = query_args.as_ref().map(|a| a.cbor());
352
353        let observer = Arc::new(StoreObserver::new(
354            &ditto,
355            &query.inner_string,
356            query_args,
357            on_change,
358        )?);
359        Ok(observer)
360    }
361
362    /// Deprecated in favour of [`ditto.store().register_observer_with_signal_next_v2()`][0]
363    ///
364    /// [0]: Store::register_observer_with_signal_next_v2
365    #[deprecated(note = "Use `ditto.store().register_observer_with_signal_next_v2()` instead")]
366    #[allow(deprecated)]
367    #[doc(hidden)]
368    pub fn register_observer_with_signal_next<Q, F>(
369        &self,
370        query: Q,
371        query_args: Option<QueryArguments>,
372        on_change: F,
373    ) -> Result<Arc<StoreObserver>, DittoError>
374    where
375        Q: TryInto<Query, Error = DittoError>,
376        F: ChangeHandlerWithSignalNext,
377    {
378        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
379
380        let query = query.try_into()?;
381        let query_args = query_args.as_ref().map(|a| a.cbor());
382
383        let new_obs = Arc::new(StoreObserver::with_signal_next(
384            &ditto,
385            &query.inner_string,
386            query_args,
387            on_change,
388        )?);
389        Ok(new_obs)
390    }
391
392    /// Installs and returns a store observer for a query, configuring Ditto to
393    /// trigger the passed in change handler whenever documents in the local
394    /// store change such that the result of the matching query changes. The
395    /// passed in query must be a `SELECT` query, otherwise an error will be
396    /// returned.
397    ///
398    /// # Example
399    ///
400    /// ```
401    /// use dittolive_ditto::prelude::*;
402    /// # fn example_with_register_observer(ditto: &Ditto) -> anyhow::Result<()> {
403    ///
404    /// let store = ditto.store();
405    /// let _observer = store.register_observer_v2(
406    ///     "SELECT * FROM cars WHERE color = 'blue'",
407    ///     move |query_result| {
408    ///         for item in query_result.iter() {
409    ///             // ... handle each item
410    ///         }
411    ///     },
412    /// )?;
413    /// # Ok(())
414    /// # }
415    /// ```
416    ///
417    /// The first invocation of the change handler will always happen after
418    /// this method has returned.
419    ///
420    /// The observer will remain active until:
421    ///
422    /// - the [`StoreObserver`] handle gets dropped,
423    /// - the [`observer.cancel()`][`StoreObserver::cancel()`] method is called, or
424    /// - the owning [`Ditto`] instance has shut down
425    ///
426    /// Observer callbacks will never be called concurrently. That is, one callback
427    /// must return before the observer will call the handler again. See
428    /// [`ditto.store().register_observer_with_signal_next_v2(...)`] if you want
429    /// to manually signal readiness for the next callback.
430    ///
431    /// [`ditto.store().register_observer_with_signal_next_v2(...)`]: crate::store::Store::register_observer_with_signal_next_v2
432    /// [`observer.cancel()`]: crate::store::StoreObserver::cancel
433    /// [`Ditto`]: crate::prelude::Ditto
434    pub fn register_observer_v2<Q, F>(
435        &self,
436        query: Q,
437        on_change: F,
438    ) -> Result<Arc<StoreObserver>, DittoError>
439    where
440        Q: IntoQuery,
441        Q::Args: Serialize,
442        F: ChangeHandler,
443    {
444        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
445        let query = query.into_query()?;
446
447        let observer = Arc::new(StoreObserver::new(
448            &ditto,
449            &query.string,
450            query.args_cbor.as_deref(),
451            on_change,
452        )?);
453        Ok(observer)
454    }
455
456    /// Installs and returns a store observer for a query, configuring Ditto to
457    /// trigger the passed in change handler whenever documents in the local
458    /// store change such that the result of the matching query changes. The
459    /// passed in query must be a `SELECT` query, otherwise an error will be
460    /// returned.
461    ///
462    /// Here, a function is passed as an additional argument to the change
463    /// handler. Call it when the handler is ready for another callback. See
464    /// [`register_observer_v2`] for a convenience method that automatically
465    /// signals the next invocation.
466    ///
467    /// # Example
468    ///
469    /// ```
470    /// use dittolive_ditto::prelude::*;
471    /// # fn example_with_signal_next(ditto: &Ditto) -> anyhow::Result<()> {
472    ///
473    /// let store = ditto.store();
474    /// let _observer = store.register_observer_with_signal_next_v2(
475    ///     "SELECT * FROM cars WHERE color = 'blue'",
476    ///     move |query_result, signal_next| {
477    ///         for item in query_result.iter() {
478    ///             // ... handle each item
479    ///         }
480    ///
481    ///         // Call `signal_next` when you're ready for the next callback
482    ///         signal_next();
483    ///     },
484    /// )?;
485    /// # Ok(())
486    /// # }
487    /// ```
488    ///
489    /// The first invocation of the change handler will always happen after
490    /// this method has returned.
491    ///
492    /// The observer will remain active until:
493    ///
494    /// - the [`StoreObserver`] handle gets dropped,
495    /// - the [`observer.cancel()`][`StoreObserver::cancel()`] method is called, or
496    /// - the owning [`Ditto`] instance has shut down
497    ///
498    /// After invoking the callback once, the observer will wait to deliver
499    /// another callback until after you've called [`signal_next`]. See [`SignalNext`]
500    /// for how this mechanism handles backpressure and collapses pending updates into at most
501    /// one callback while delivery is paused.
502    ///
503    /// [`register_observer_v2`]: Store::register_observer_v2
504    /// [`signal_next`]: crate::store::SignalNext
505    pub fn register_observer_with_signal_next_v2<Q, F>(
506        &self,
507        query: Q,
508        on_change: F,
509    ) -> Result<Arc<StoreObserver>, DittoError>
510    where
511        Q: IntoQuery,
512        Q::Args: Serialize,
513        F: ChangeHandlerWithSignalNext,
514    {
515        let ditto = Ditto::upgrade(&self.weak_ditto_fields)?;
516        let query = query.into_query()?;
517
518        let new_obs = Arc::new(StoreObserver::with_signal_next(
519            &ditto,
520            &query.string,
521            query.args_cbor.as_deref(),
522            on_change,
523        )?);
524        Ok(new_obs)
525    }
526
527    /// Gets temporary access to the set of currently registered observers.
528    ///
529    /// A (read) lock is held until the return value is dropped: this means
530    /// that neither [`Self::register_observer()`] nor
531    /// [`StoreObserver::cancel()`] can make progress until this read
532    /// lock is released.
533    pub fn observers(&self) -> impl '_ + Deref<Target = SetArc<StoreObserver>> {
534        let observers: repr_c::Vec<repr_c::Box<FfiStoreObserver>> =
535            ffi_sdk::dittoffi_store_observers(&self.ditto);
536        let observers: Vec<_> = observers.into();
537        let observers = observers
538            .into_iter()
539            .map(|handle: repr_c::Box<FfiStoreObserver>| Arc::new(StoreObserver { handle }))
540            .collect::<SetArc<_>>();
541
542        Box::new(observers)
543    }
544
545    /// Executes the given query in the local store and returns the result.
546    ///
547    /// Use placeholders to incorporate values from the optional `query_args`
548    /// parameter into the query. The keys of the [`QueryArguments`] object must
549    /// match the placeholders used within the query. You can not use placeholders
550    /// in the `FROM` clause.
551    ///
552    /// This method only returns results from the local store without waiting for any
553    /// [`SyncSubscription`]s to have caught up with the
554    /// latest changes. Only use this method if your program must proceed with immediate results.
555    ///
556    /// Use [`ditto.store().register_observer(...)`] to receive updates to query results
557    /// as soon as they have been synced to this peer.
558    ///
559    /// Limitations:
560    ///
561    /// - Supports `SELECT * FROM <collection name>` with optional `WHERE <expression>`
562    /// - No transactions
563    ///
564    /// [`SyncSubscription`]: crate::sync::SyncSubscription
565    /// [`ditto.store().register_observer(...)`]: crate::store::Store::register_observer
566    #[deprecated = "Use `ditto.store().execute_v2(...)` instead"]
567    #[allow(deprecated)]
568    #[doc(hidden)]
569    pub async fn execute<Q>(
570        &self,
571        query: Q,
572        query_args: Option<QueryArguments>,
573    ) -> Result<QueryResult, DittoError>
574    where
575        Q: TryInto<Query, Error = DittoError>,
576    {
577        // Get a DqlResult. The operation to get it is fallible, so we end up
578        // with a `FfiResult<DqlResult> -> Result<DqlResult> -> DqlResult`.
579        // The `Result` stutter is quite unfortunate, which _results_ from
580        // having picked that name in our public SDK API (over, say, `Output`).
581        let ffi_query_result = ffi_sdk::dittoffi_try_exec_statement(
582            &self.ditto,
583            query.try_into()?.prepare_ffi(),
584            query_args.as_ref().map(|qa| qa.cbor().into()),
585        )
586        .into_rust_result()?;
587
588        Ok(QueryResult::from(ffi_query_result))
589    }
590
591    /// Executes the given query in the local store and returns the result.
592    ///
593    /// # Example
594    ///
595    /// ```
596    /// use dittolive_ditto::prelude::*;
597    /// use dittolive_ditto::dql::QueryResult;
598    /// # async fn example(ditto: &Ditto) -> anyhow::Result<()> {
599    ///
600    /// // Query a collection
601    /// let result = ditto.store().execute_v2("SELECT * FROM cars").await?;
602    ///
603    /// // Insert a document into a collection
604    /// let insert_result: QueryResult = ditto
605    ///     .store()
606    ///     .execute_v2((
607    ///          "INSERT INTO cars DOCUMENTS (:newCar)",
608    ///          serde_json::json!({
609    ///              "newCar": {
610    ///                  "make": "ford",
611    ///                  "color": "blue"
612    ///              }
613    ///          })
614    ///     ))
615    ///     .await?;
616    /// # Ok(())
617    /// # }
618    /// ```
619    ///
620    /// Use placeholders to incorporate values from the optional `query_args`
621    /// parameter into the query. The keys of the [`QueryArguments`] object must
622    /// match the placeholders used within the query. You can not use placeholders
623    /// in the `FROM` clause.
624    ///
625    /// This method only returns results from the local store without waiting for any
626    /// [`SyncSubscription`]s to have caught up with the
627    /// latest changes. Only use this method if your program must proceed with immediate results.
628    ///
629    /// Use [`ditto.store().register_observer(...)`] to receive updates to query results
630    /// as soon as they have been synced to this peer.
631    ///
632    /// ## Query parameter
633    ///
634    /// The `query` parameter must implement [`IntoQuery`], which is a trait that is implemented by
635    /// objects that can be turned into a query string along with the relevant query argumnents.
636    ///
637    /// For queries with no arguments, a [`String`] is sufficient
638    ///
639    /// [`SyncSubscription`]: crate::sync::SyncSubscription
640    /// [`ditto.store().register_observer(...)`]: crate::store::Store::register_observer
641    pub async fn execute_v2<Q>(&self, query: Q) -> Result<QueryResult, DittoError>
642    where
643        Q: IntoQuery,
644        Q::Args: serde::Serialize,
645    {
646        let query = query.into_query()?;
647        let query_string = (&*query.string).into();
648        let query_args = query.args_cbor.as_deref().map(Into::into);
649
650        let ffi_query_result =
651            ffi_sdk::dittoffi_try_exec_statement(&self.ditto, query_string, query_args)
652                .into_rust_result()?;
653
654        Ok(QueryResult::from(ffi_query_result))
655    }
656
657    /// Creates a new attachment, which can then be inserted into a document.
658    ///
659    /// The file residing at the provided path will be copied into Ditto’s store. The
660    /// [`DittoAttachment`] object that is returned is what you can
661    /// then use to insert an attachment into a document.
662    ///
663    /// You can provide custom user data about the attachment, which will be replicated to other
664    /// peers alongside the file attachment.
665    pub async fn new_attachment(
666        &self,
667        filepath: &(impl ?Sized + AsRef<Path>),
668        user_data: HashMap<String, String>,
669    ) -> Result<DittoAttachment, DittoError> {
670        DittoAttachment::from_file_and_metadata(filepath, user_data, &self.ditto)
671    }
672
673    /// Creates a new attachment from in-memory data
674    ///
675    /// Refer to [`new_attachment`](Self::new_attachment) for additional information.
676    pub async fn new_attachment_from_bytes(
677        &self,
678        bytes: &(impl ?Sized + AsRef<[u8]>),
679        user_data: HashMap<String, String>,
680    ) -> Result<DittoAttachment, DittoError> {
681        DittoAttachment::from_bytes_and_metadata(bytes, user_data, &self.ditto)
682    }
683
684    /// Fetches the attachment corresponding to the provided attachment token.
685    /// - `attachment_token`: can be either a [`DittoAttachmentToken`], or a `&BTreeMap<CborValue,
686    ///   CborValue>`, that is, the output of a [`QueryResultItem::value()`] once casted
687    ///   [`.as_object()`][crate::prelude::CborValueGetters::as_object()].
688    ///
689    /// - `on_fetch_event`: A closure that will be called when the status of the request to fetch
690    ///   the attachment has changed. If the attachment is already available then this will be
691    ///   called almost immediately with a completed status value.
692    ///
693    /// The returned [`DittoAttachmentFetcher`] is a handle which is safe to discard, unless you
694    /// wish to be able to [`.cancel()`][DittoAttachmentFetcher::cancel] the fetching operation.
695    /// When not explicitly cancelled, the fetching operation will remain active until it either
696    /// completes, the attachment is deleted, or the owning [`Ditto`] object is dropped.
697    pub fn fetch_attachment(
698        &self,
699        attachment_token: impl attachment::token::DittoAttachmentTokenLike,
700        on_fetch_event: impl 'static + Send + Sync + Fn(DittoAttachmentFetchEvent),
701    ) -> Result<DittoAttachmentFetcherV2, DittoError> {
702        let attachment_token = attachment_token.parse_attachment_token()?;
703
704        let weak_ditto = self.weak_ditto_fields.clone();
705        let ditto = weak_ditto
706            .upgrade()
707            .ok_or(ErrorKind::ReleasedDittoInstance)?;
708
709        let mut attachment_fetchers_lockguard = self.attachment_fetchers.write().unwrap();
710        let fetcher = DittoAttachmentFetcher::new(
711            attachment_token,
712            Some(&ditto),
713            &self.ditto,
714            // Shim around `on_fetch_event` to `cancel` on completion.
715            move |event, cancel_token: &AtomicU64| {
716                #[allow(clippy::unneeded_struct_pattern)]
717                let has_finished = matches! {
718                    event,
719                    | DittoAttachmentFetchEvent::Completed { .. }
720                    | DittoAttachmentFetchEvent::Deleted { .. }
721                };
722                on_fetch_event(event);
723                if has_finished {
724                    if let Some(ditto) = weak_ditto.upgrade() {
725                        let mut attachment_fetchers_inner_lockguard =
726                            ditto.store.attachment_fetchers.write().unwrap();
727                        // Relaxed is fine thanks to the lock.
728                        let cancel_token = cancel_token.load(atomic::Ordering::Relaxed);
729                        ditto.store.unregister_fetcher(
730                            cancel_token,
731                            Some(&mut *attachment_fetchers_inner_lockguard),
732                        );
733                    }
734                }
735            },
736        )?;
737        let (cancel_token, was_zero) = fetcher.cancel_token_ensure_unique();
738        attachment_fetchers_lockguard.insert(cancel_token, (was_zero, fetcher.clone()));
739        Ok(fetcher)
740    }
741
742    fn unregister_fetcher(
743        &self,
744        mut fetcher_cancel_token: CancelToken,
745        fetchers: Option<&mut HashMap<CancelToken, (bool, DittoAttachmentFetcherV2)>>,
746    ) -> bool {
747        let mut lock_guard = None;
748        let fetchers = fetchers.unwrap_or_else(|| {
749            &mut **lock_guard.get_or_insert(self.attachment_fetchers.write().unwrap())
750        });
751
752        let Some((was_zero, removed_fetcher)) = fetchers.remove(&fetcher_cancel_token) else {
753            return false;
754        };
755        drop(lock_guard);
756
757        if was_zero {
758            fetcher_cancel_token = 0;
759        }
760
761        let att_token = &removed_fetcher.context.token;
762        debug!(
763            token_id = %att_token.id(),
764            %fetcher_cancel_token,
765            "unregistering ditto attachment fetcher"
766        );
767
768        let status = ffi_sdk::ditto_cancel_resolve_attachment(
769            &self.ditto,
770            att_token.id.as_ref().into(),
771            fetcher_cancel_token,
772        );
773
774        if status != 0 {
775            error!(
776                token_id = %att_token.id(),
777                %fetcher_cancel_token,
778                "failed to clean up attachment fetcher"
779            );
780        }
781        status == 0
782    }
783
784    /// Gets a copy of the set of currently registered attachment fetchers.
785    ///
786    /// A (read) lock is held during the copy: this contends with [`Self::fetch_attachment()`] and
787    /// with [`DittoAttachmentFetcher::cancel()`].
788    pub fn attachment_fetchers(&self) -> Vec<DittoAttachmentFetcherV2> {
789        self.attachment_fetchers
790            .read()
791            .unwrap()
792            .iter()
793            .map(|(_, (_, fetcher))| fetcher.clone())
794            .collect()
795    }
796}
797
798/// Specify the order of returned Documents in a query.
799#[non_exhaustive]
800#[derive(Clone, Copy, PartialEq, Eq, Debug)]
801#[deprecated(since = "4.14.4", note = "Will be removed in SDK 5.0")]
802pub enum SortDirection {
803    /// First result is "smallest", last result is "largest"
804    Ascending,
805
806    /// First result is "largest", last result is "smallest"
807    Descending,
808}
809
810#[deprecated(since = "4.14.4", note = "Will be removed in SDK 5.0")]
811#[non_exhaustive]
812#[derive(Clone, Copy, PartialEq, Eq, Debug)]
813/// Specify the write strategy when inserting documents.
814pub enum WriteStrategy {
815    /// An existing document will be merged with the document being inserted, if there is a
816    /// pre-existing document.
817    Merge,
818
819    /// Insert the document only if there is not already a document with the same Id in the store.
820    /// If there is already a document in the store with the same Id then this will be a no-op.
821    InsertIfAbsent,
822
823    /// Insert the document, with its contents treated as default data, only if there is not
824    /// already a document with the same Id in the store. If there is already a document in the
825    /// store with the same Id then this will be a no-op. Use this strategy if you want to
826    /// insert default data for a given document Id, which you want to treat as common initial
827    /// data amongst all peers and that you expect to be mutated or overwritten in due course.
828    InsertDefaultIfAbsent,
829}
830
831#[allow(deprecated)]
832impl WriteStrategy {
833    fn as_write_strategy_rs(&self) -> WriteStrategyRs {
834        match self {
835            WriteStrategy::Merge => WriteStrategyRs::Merge,
836            WriteStrategy::InsertIfAbsent => WriteStrategyRs::InsertIfAbsent,
837            WriteStrategy::InsertDefaultIfAbsent => WriteStrategyRs::InsertDefaultIfAbsent,
838        }
839    }
840}