1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use ::ffi_sdk::{self, ffi_utils::repr_c};
use ::serde::de::DeserializeOwned;
use ::std::sync::{Arc, Mutex};

use crate::{error::DittoError, store::DocumentId};

type CborObject = ::std::collections::HashMap<Box<str>, ::serde_cbor::Value>;

/// Represents results returned when executing a DQL query containing
/// a [`QueryResultItem`] for each match.
///
/// > Note: More info such as metrics, affected document IDs, etc. will be
/// provided in the near future.
pub struct QueryResult {
    raw: repr_c::Box<ffi_sdk::QueryResult>,
    count: usize,
}

impl From<repr_c::Box_<ffi_sdk::QueryResult>> for QueryResult {
    fn from(raw: repr_c::Box<ffi_sdk::QueryResult>) -> QueryResult {
        let count = ffi_sdk::dittoffi_query_result_item_count(&raw);
        QueryResult { raw, count }
    }
}

impl QueryResult {
    /// Get the [`QueryResultItem`] at the given index.
    /// Return [`None`] if out of bounds.
    pub fn get_item(&self, index: usize) -> Option<QueryResultItem> {
        if index >= self.count {
            return None;
        }
        Some(QueryResultItem::from(
            ffi_sdk::dittoffi_query_result_item_at(&self.raw, index),
        ))
    }

    /// Return the number of available [`QueryResultItem`].
    pub fn item_count(&self) -> usize {
        self.count
    }

    /// IDs of documents that were mutated by the DQL query. Empty
    /// array if no documents have been mutated.
    ///
    /// > Important: The returned document IDs are not cached, make sure to call
    /// this method once and keep the return value for as long as needed.
    pub fn mutated_document_ids(&self) -> Vec<DocumentId> {
        let mutated_document_number =
            ffi_sdk::dittoffi_query_result_mutated_document_id_count(&self.raw);

        (0..mutated_document_number)
            .map(|idx| ffi_sdk::dittoffi_query_result_mutated_document_id_at(&self.raw, idx))
            .map(|raw_slice| DocumentId::from(Box::<[u8]>::from(raw_slice)))
            .collect()
    }
}

impl QueryResult {
    pub fn iter(&self) -> impl '_ + Iterator<Item = QueryResultItem> {
        self.into_iter()
    }
}

mod sealed {
    pub struct QueryResultIterator<'iter> {
        pub(super) query_result: &'iter super::QueryResult,
        pub(super) idx: usize,
    }
}
use self::sealed::QueryResultIterator;

impl<'iter> IntoIterator for &'iter QueryResult {
    type IntoIter = QueryResultIterator<'iter>;
    type Item = QueryResultItem;

    fn into_iter(self) -> QueryResultIterator<'iter> {
        QueryResultIterator {
            query_result: self,
            idx: 0,
        }
    }
}

impl Iterator for QueryResultIterator<'_> {
    type Item = QueryResultItem;

    fn next(&mut self) -> Option<Self::Item> {
        let return_value = self.query_result.get_item(self.idx);
        if return_value.is_some() {
            self.idx += 1;
        }
        return_value
    }
}

/// Represents a single match of a DQL query, similar to a "row" in SQL terms.
/// It's a reference type serving as a "cursor", allowing for efficient access
/// of the underlying data in various formats.
///
/// The row is lazily materialized and kept in memory until it goes out of scope.
/// To reduce the **memory footprint**, access the items using the provided iterator.
///
/// ```
/// # use serde::Deserialize;
/// # use dittolive_ditto::store::dql::QueryResult;
/// # #[derive(Deserialize)]
/// # struct Car{}
/// # fn scope(all_cars_query_result: QueryResult) {
/// let cars: Vec<Car> = all_cars_query_result
///     .iter()
///     .map(|query_result_item| query_result_item.deserialize_value().unwrap())
///     .collect();
/// # }
/// ```
pub struct QueryResultItem {
    /// Raw pointer to the core QueryResultItem
    raw: repr_c::Box<ffi_sdk::QueryResultItem>,
    materialized_value: Mutex<Option<Arc<CborObject>>>,
}

impl From<repr_c::Box_<ffi_sdk::QueryResultItem>> for QueryResultItem {
    fn from(raw: repr_c::Box<ffi_sdk::QueryResultItem>) -> Self {
        Self {
            raw,
            materialized_value: <_>::default(),
        }
    }
}

impl QueryResultItem {
    /// Returns the content as a materialized object.
    ///
    /// The item's value is [`.materialize()`]-ed on first access and
    /// subsequently on each access after performing [`.dematerialize()`]. Once
    /// materialized, the value is kept in memory until explicitly
    /// [`.dematerialize()`]-ed or the item goes out of scope.
    ///
    /// [`.materialize()`]: Self::materialize
    /// [`.dematerialize()`]: Self::dematerialize
    pub fn value(&self) -> Arc<CborObject> {
        let cache = &mut *self.materialized_value.lock().unwrap();
        Self::materialize_(&self.raw, cache).clone()
    }

    /// Returns `true` if value is currently held materialized in memory,
    /// otherwise returns `false`.
    ///
    /// ### See Also
    ///
    /// - [`Self::materialize()`]
    /// - [`Self::dematerialize()`]
    pub fn is_materialized(&self) -> bool {
        self.materialized_value.lock().unwrap().is_some()
    }

    /// Common helper to `.value()` and `.materialize()`.
    fn materialize_<'cache>(
        raw: &ffi_sdk::QueryResultItem,
        cache: &'cache mut Option<Arc<CborObject>>,
    ) -> &'cache Arc<CborObject> {
        cache.get_or_insert_with(|| {
            let cbor_data = ffi_sdk::dittoffi_query_result_item_cbor(raw);
            Arc::new(::serde_cbor::from_slice(&cbor_data[..]).expect(
                "internal inconsistency, couldn't materialize query result item due to CBOR \
                 decoding error",
            ))
        })
    }

    /// Loads the CBOR representation of the item's content, decodes it as a
    /// dictionary so it can be accessed via [`.value()`]. Keeps the dictionary in
    /// memory until [`.dematerialize()`] is called. No-op if `value` is already
    /// materialized.
    ///
    /// [`.value()`]: Self::value
    /// [`.dematerialize()`]: Self::dematerialize
    pub fn materialize(&mut self) {
        Self::materialize_(&self.raw, self.materialized_value.get_mut().unwrap());
    }

    /// Releases the materialized value from memory. No-op if item is not
    /// materialized.
    pub fn dematerialize(&mut self) {
        *self.materialized_value.get_mut().unwrap() = None;
    }

    /// Return the content of the item as a CBOR slice.
    ///
    /// *Important*: The returned CBOR slice is not cached, make sure to call this method once and
    /// keep it for as long as needed.
    pub fn cbor_data(&self) -> Vec<u8> {
        let c_slice = ffi_sdk::dittoffi_query_result_item_cbor(&self.raw);
        Box::<[u8]>::from(c_slice).into()
    }

    /// Return the content of the item as a JSON string.
    ///
    /// *Important*: The returned JSON string is not cached, make sure to call this method once and
    /// keep it for as long as needed.
    pub fn json_string(&self) -> String {
        let raw_string = ffi_sdk::dittoffi_query_result_item_json(&self.raw);
        raw_string.into_string()
    }

    /// Convenience around [`Self::cbor_data()`] `deserialize`-ing the value.
    ///
    /// *Important*: The returned value is not cached, make sure to call this method once and
    /// keep it for as long as needed.
    pub fn deserialize_value<T: DeserializeOwned>(&self) -> Result<T, DittoError> {
        ::serde_cbor::from_slice(&self.cbor_data()).map_err(Into::into)
    }
}