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