dittolive-ditto 4.13.0

Ditto is a peer to peer cross-platform database that allows mobile, web, IoT and server apps to sync with or without an internet connection.
Documentation
use std::{
    num::NonZeroU64,
    sync::{Arc, Mutex},
};

use ffi_sdk::{self, ffi_utils::repr_c};
use serde::de::DeserializeOwned;

// TODO(v5): remove
#[doc(hidden)]
#[allow(deprecated)]
use crate::store::collection::document_id::DocumentId;
use crate::{error::DittoError, utils::extension_traits::FfiResultIntoRustResult};

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::FfiQueryResult>,
    count: usize,
}

impl From<repr_c::Box_<ffi_sdk::FfiQueryResult>> for QueryResult {
    fn from(raw: repr_c::Box<ffi_sdk::FfiQueryResult>) -> 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()
    }

    /// The commit ID associated with this query result, if any.
    ///
    /// This ID uniquely identifies the commit in which this change was accepted
    /// into the _local_ store. The commit ID is available for all query results
    /// involving insertions, updates, or deletions. This ID can be used to track
    /// whether a local change has been synced to other peers.
    ///
    /// For write transactions, the commit ID is only available after the
    /// transaction has been successfully committed. Queries executed within an
    /// uncommitted transaction will not have a commit ID.
    pub fn commit_id(&self) -> Option<NonZeroU64> {
        NonZeroU64::new(ffi_sdk::dittoffi_query_result_commit_id(&self.raw))
    }
}

impl QueryResult {
    /// Create an iterator over [`QueryResultItem`]s
    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::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
    pub(super) raw: repr_c::Arc_<ffi_sdk::FfiQueryResultItem>,
    materialized_value: Mutex<Option<Arc<CborObject>>>,
}

impl From<repr_c::Arc_<ffi_sdk::FfiQueryResultItem>> for QueryResultItem {
    fn from(raw: repr_c::Arc<ffi_sdk::FfiQueryResultItem>) -> 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::FfiQueryResultItem,
        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)
    }

    /// Create a new [`QueryResultItem`] from a [`serde_json::Value`].
    ///
    /// This is available for testing purposes, but should not be used in production code. This API
    /// may change or be removed in the future.
    #[doc(hidden)]
    pub fn unstable_try_from_serde_json_value(
        value: serde_json::Value,
    ) -> Result<Self, DittoError> {
        let json_data = value.to_string().into_bytes().to_vec();
        let raw = ffi_sdk::dittoffi_query_result_item_new(json_data.as_slice().into())
            .into_rust_result()?;
        Ok(QueryResultItem {
            raw,
            materialized_value: Mutex::new(None),
        })
    }
}

#[cfg(test)]
mod tests {
    use serde_json::json;

    use super::*;
    use crate::prelude::CborValueGetters;

    #[test]
    fn create_query_result_item_from_json() {
        let json_dict = json!({"_id": "1", "data": "A"});
        let query_result_item =
            QueryResultItem::unstable_try_from_serde_json_value(json_dict).unwrap();
        let value = query_result_item.value();
        assert_eq!(value["_id"].as_str().unwrap(), "1");
        assert_eq!(value["data"].as_str().unwrap(), "A");
    }
}