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}