dittolive_ditto/dql/
query_result.rs

1use std::{
2    num::NonZeroU64,
3    sync::{Arc, Mutex},
4};
5
6use ffi_sdk::{self, ffi_utils::repr_c};
7use serde::de::DeserializeOwned;
8
9// TODO(v5): remove
10#[doc(hidden)]
11#[allow(deprecated)]
12use crate::store::collection::document_id::DocumentId;
13use crate::{error::DittoError, utils::extension_traits::FfiResultIntoRustResult};
14
15type CborObject = ::std::collections::HashMap<Box<str>, ::serde_cbor::Value>;
16
17/// Represents results returned when executing a DQL query containing
18/// a [`QueryResultItem`] for each match.
19///
20/// > Note: More info such as metrics, affected document IDs, etc. will be
21/// > provided in the near future.
22pub struct QueryResult {
23    raw: repr_c::Box<ffi_sdk::FfiQueryResult>,
24    count: usize,
25}
26
27impl From<repr_c::Box_<ffi_sdk::FfiQueryResult>> for QueryResult {
28    fn from(raw: repr_c::Box<ffi_sdk::FfiQueryResult>) -> QueryResult {
29        let count = ffi_sdk::dittoffi_query_result_item_count(&raw);
30        QueryResult { raw, count }
31    }
32}
33
34impl QueryResult {
35    /// Get the [`QueryResultItem`] at the given index.
36    /// Return [`None`] if out of bounds.
37    pub fn get_item(&self, index: usize) -> Option<QueryResultItem> {
38        if index >= self.count {
39            return None;
40        }
41        Some(QueryResultItem::from(
42            ffi_sdk::dittoffi_query_result_item_at(&self.raw, index),
43        ))
44    }
45
46    /// Return the number of available [`QueryResultItem`].
47    pub fn item_count(&self) -> usize {
48        self.count
49    }
50
51    /// IDs of documents that were mutated by the DQL query. Empty
52    /// array if no documents have been mutated.
53    ///
54    /// > Important: The returned document IDs are not cached, make sure to call
55    /// > this method once and keep the return value for as long as needed.
56    pub fn mutated_document_ids(&self) -> Vec<DocumentId> {
57        let mutated_document_number =
58            ffi_sdk::dittoffi_query_result_mutated_document_id_count(&self.raw);
59
60        (0..mutated_document_number)
61            .map(|idx| ffi_sdk::dittoffi_query_result_mutated_document_id_at(&self.raw, idx))
62            .map(|raw_slice| DocumentId::from(Box::<[u8]>::from(raw_slice)))
63            .collect()
64    }
65
66    /// The commit ID associated with this query result, if any.
67    ///
68    /// This ID uniquely identifies the commit in which this change was accepted
69    /// into the _local_ store. The commit ID is available for all query results
70    /// involving insertions, updates, or deletions. This ID can be used to track
71    /// whether a local change has been synced to other peers.
72    ///
73    /// For write transactions, the commit ID is only available after the
74    /// transaction has been successfully committed. Queries executed within an
75    /// uncommitted transaction will not have a commit ID.
76    pub fn commit_id(&self) -> Option<NonZeroU64> {
77        NonZeroU64::new(ffi_sdk::dittoffi_query_result_commit_id(&self.raw))
78    }
79}
80
81impl QueryResult {
82    /// Create an iterator over [`QueryResultItem`]s
83    pub fn iter(&self) -> impl '_ + Iterator<Item = QueryResultItem> {
84        self.into_iter()
85    }
86}
87
88mod sealed {
89    pub struct QueryResultIterator<'iter> {
90        pub(super) query_result: &'iter super::QueryResult,
91        pub(super) idx: usize,
92    }
93}
94use self::sealed::QueryResultIterator;
95
96impl<'iter> IntoIterator for &'iter QueryResult {
97    type IntoIter = QueryResultIterator<'iter>;
98    type Item = QueryResultItem;
99
100    fn into_iter(self) -> QueryResultIterator<'iter> {
101        QueryResultIterator {
102            query_result: self,
103            idx: 0,
104        }
105    }
106}
107
108impl Iterator for QueryResultIterator<'_> {
109    type Item = QueryResultItem;
110
111    fn next(&mut self) -> Option<Self::Item> {
112        let return_value = self.query_result.get_item(self.idx);
113        if return_value.is_some() {
114            self.idx += 1;
115        }
116        return_value
117    }
118}
119
120/// Represents a single match of a DQL query, similar to a "row" in SQL terms.
121/// It's a reference type serving as a "cursor", allowing for efficient access
122/// of the underlying data in various formats.
123///
124/// The row is lazily materialized and kept in memory until it goes out of scope.
125/// To reduce the **memory footprint**, access the items using the provided iterator.
126///
127/// ```
128/// # use serde::Deserialize;
129/// # use dittolive_ditto::dql::QueryResult;
130/// # #[derive(Deserialize)]
131/// # struct Car{}
132/// # fn scope(all_cars_query_result: QueryResult) {
133/// let cars: Vec<Car> = all_cars_query_result
134///     .iter()
135///     .map(|query_result_item| query_result_item.deserialize_value().unwrap())
136///     .collect();
137/// # }
138/// ```
139pub struct QueryResultItem {
140    /// Raw pointer to the core QueryResultItem
141    pub(super) raw: repr_c::Arc_<ffi_sdk::FfiQueryResultItem>,
142    materialized_value: Mutex<Option<Arc<CborObject>>>,
143}
144
145impl From<repr_c::Arc_<ffi_sdk::FfiQueryResultItem>> for QueryResultItem {
146    fn from(raw: repr_c::Arc<ffi_sdk::FfiQueryResultItem>) -> Self {
147        Self {
148            raw,
149            materialized_value: <_>::default(),
150        }
151    }
152}
153
154impl QueryResultItem {
155    /// Returns the content as a materialized object.
156    ///
157    /// The item's value is [`.materialize()`]-ed on first access and
158    /// subsequently on each access after performing [`.dematerialize()`]. Once
159    /// materialized, the value is kept in memory until explicitly
160    /// [`.dematerialize()`]-ed or the item goes out of scope.
161    ///
162    /// [`.materialize()`]: Self::materialize
163    /// [`.dematerialize()`]: Self::dematerialize
164    pub fn value(&self) -> Arc<CborObject> {
165        let cache = &mut *self.materialized_value.lock().unwrap();
166        Self::materialize_(&self.raw, cache).clone()
167    }
168
169    /// Returns `true` if value is currently held materialized in memory,
170    /// otherwise returns `false`.
171    ///
172    /// ### See Also
173    ///
174    /// - [`Self::materialize()`]
175    /// - [`Self::dematerialize()`]
176    pub fn is_materialized(&self) -> bool {
177        self.materialized_value.lock().unwrap().is_some()
178    }
179
180    /// Common helper to `.value()` and `.materialize()`.
181    fn materialize_<'cache>(
182        raw: &ffi_sdk::FfiQueryResultItem,
183        cache: &'cache mut Option<Arc<CborObject>>,
184    ) -> &'cache Arc<CborObject> {
185        cache.get_or_insert_with(|| {
186            let cbor_data = ffi_sdk::dittoffi_query_result_item_cbor(raw);
187            Arc::new(::serde_cbor::from_slice(&cbor_data[..]).expect(
188                "internal inconsistency, couldn't materialize query result item due to CBOR \
189                 decoding error",
190            ))
191        })
192    }
193
194    /// Loads the CBOR representation of the item's content, decodes it as a
195    /// dictionary so it can be accessed via [`.value()`]. Keeps the dictionary in
196    /// memory until [`.dematerialize()`] is called. No-op if `value` is already
197    /// materialized.
198    ///
199    /// [`.value()`]: Self::value
200    /// [`.dematerialize()`]: Self::dematerialize
201    pub fn materialize(&mut self) {
202        Self::materialize_(&self.raw, self.materialized_value.get_mut().unwrap());
203    }
204
205    /// Releases the materialized value from memory. No-op if item is not
206    /// materialized.
207    pub fn dematerialize(&mut self) {
208        *self.materialized_value.get_mut().unwrap() = None;
209    }
210
211    /// Return the content of the item as a CBOR slice.
212    ///
213    /// *Important*: The returned CBOR slice is not cached, make sure to call this method once and
214    /// keep it for as long as needed.
215    pub fn cbor_data(&self) -> Vec<u8> {
216        let c_slice = ffi_sdk::dittoffi_query_result_item_cbor(&self.raw);
217        Box::<[u8]>::from(c_slice).into()
218    }
219
220    /// Return the content of the item as a JSON string.
221    ///
222    /// *Important*: The returned JSON string is not cached, make sure to call this method once and
223    /// keep it for as long as needed.
224    pub fn json_string(&self) -> String {
225        let raw_string = ffi_sdk::dittoffi_query_result_item_json(&self.raw);
226        raw_string.into_string()
227    }
228
229    /// Convenience around [`Self::cbor_data()`] `deserialize`-ing the value.
230    ///
231    /// *Important*: The returned value is not cached, make sure to call this method once and
232    /// keep it for as long as needed.
233    pub fn deserialize_value<T: DeserializeOwned>(&self) -> Result<T, DittoError> {
234        ::serde_cbor::from_slice(&self.cbor_data()).map_err(Into::into)
235    }
236
237    /// Create a new [`QueryResultItem`] from a [`serde_json::Value`].
238    ///
239    /// This is available for testing purposes, but should not be used in production code. This API
240    /// may change or be removed in the future.
241    #[doc(hidden)]
242    pub fn unstable_try_from_serde_json_value(
243        value: serde_json::Value,
244    ) -> Result<Self, DittoError> {
245        let json_data = value.to_string().into_bytes().to_vec();
246        let raw = ffi_sdk::dittoffi_query_result_item_new(json_data.as_slice().into())
247            .into_rust_result()?;
248        Ok(QueryResultItem {
249            raw,
250            materialized_value: Mutex::new(None),
251        })
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use serde_json::json;
258
259    use super::*;
260    use crate::prelude::CborValueGetters;
261
262    #[test]
263    fn create_query_result_item_from_json() {
264        let json_dict = json!({"_id": "1", "data": "A"});
265        let query_result_item =
266            QueryResultItem::unstable_try_from_serde_json_value(json_dict).unwrap();
267        let value = query_result_item.value();
268        assert_eq!(value["_id"].as_str().unwrap(), "1");
269        assert_eq!(value["data"].as_str().unwrap(), "A");
270    }
271}