bonsaidb-core 0.4.1

Core functionality and types for BonsaiDb.
Documentation
use arc_bytes::serde::Bytes;
use serde::{Deserialize, Serialize};

mod timestamp;

pub use self::timestamp::Timestamp;
use crate::Error;

mod implementation {
    use arc_bytes::serde::Bytes;
    use async_trait::async_trait;
    use futures::future::BoxFuture;
    use serde::Serialize;

    use crate::{
        keyvalue::{Command, KeyCheck, KeyOperation, KeyStatus, Output, Timestamp},
        Error,
    };

    /// Types for executing get operations.
    pub mod get;
    /// Types for executing increment/decrement operations.
    pub mod increment;
    /// Types for handling key namespaces.
    pub mod namespaced;
    /// Types for executing set operations.
    pub mod set;

    use namespaced::Namespaced;

    use super::{IncompatibleTypeError, Numeric, Value};
    /// Key-Value store methods. The Key-Value store is designed to be a
    /// high-performance, lightweight storage mechanism.
    ///
    /// When compared to Collections, the Key-Value store does not offer
    /// ACID-compliant transactions. Instead, the Key-Value store is made more
    /// efficient by periodically flushing the store to disk rather than during
    /// each operation. As such, the Key-Value store is intended to be used as a
    /// lightweight caching layer. However, because each of the operations it
    /// supports are executed atomically, the Key-Value store can also be
    /// utilized for synchronized locking.
    ///
    /// ## Floating Point Operations
    ///
    /// When using [`KeyValue::set_numeric_key()`] or any numeric operations, if
    /// a [Not a Number (NaN) value][nan] is encountered, [`Error::NotANumber`]
    /// will be returned without allowing the operation to succeed.
    ///
    /// Positive and negative infinity values are allowed, as they do not break
    /// comparison operations.
    ///
    /// [nan]: https://en.wikipedia.org/wiki/NaN
    pub trait KeyValue: Sized + Send + Sync {
        /// Executes a single [`KeyOperation`].
        fn execute_key_operation(&self, op: KeyOperation) -> Result<Output, Error>;

        /// Sets `key` to `value`. This function returns a builder that is also a
        /// Future. Awaiting the builder will execute [`Command::Set`] with the options
        /// given.
        fn set_key<'a, S: Into<String>, V: Serialize + Send + Sync>(
            &'a self,
            key: S,
            value: &'a V,
        ) -> set::Builder<'a, Self, V> {
            set::Builder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Serializeable(value),
            )
        }

        /// Sets `key` to `bytes`. This function returns a builder that is also
        /// a Future. Awaiting the builder will execute [`Command::Set`] with
        /// the options given.
        fn set_binary_key<'a, S: Into<String>>(
            &'a self,
            key: S,
            bytes: &'a [u8],
        ) -> set::Builder<'a, Self, ()> {
            set::Builder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Bytes(bytes),
            )
        }

        /// Sets `key` to `value`. This stores the value as a `Numeric`,
        /// enabling atomic math operations to be performed on this key. This
        /// function returns a builder that is also a Future. Awaiting the
        /// builder will execute [`Command::Set`] with the options given.
        fn set_numeric_key<S: Into<String>, V: Into<Numeric>>(
            &self,
            key: S,
            value: V,
        ) -> set::Builder<'_, Self, ()> {
            set::Builder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Numeric(value.into()),
            )
        }

        /// Increments `key` by `value`. The value stored must be a `Numeric`,
        /// otherwise an error will be returned. The result of the increment
        /// will be the `value`'s type. For example, if the stored value is
        /// currently a `u64`, but `value` is a `f64`, the current value will be
        /// converted to an `f64`, and the stored value will be an `f64`.
        fn increment_key_by<
            S: Into<String> + Send + Sync,
            V: Into<Numeric> + TryFrom<Numeric, Error = IncompatibleTypeError> + Send + Sync,
        >(
            &self,
            key: S,
            value: V,
        ) -> increment::Builder<'_, Self, V> {
            increment::Builder::new(
                self,
                self.key_namespace().map(Into::into),
                true,
                key.into(),
                value.into(),
            )
        }

        /// Decrements `key` by `value`. The value stored must be a `Numeric`,
        /// otherwise an error will be returned. The result of the decrement
        /// will be the `value`'s type. For example, if the stored value is
        /// currently a `u64`, but `value` is a `f64`, the current value will be
        /// converted to an `f64`, and the stored value will be an `f64`.
        fn decrement_key_by<
            S: Into<String> + Send + Sync,
            V: Into<Numeric> + TryFrom<Numeric, Error = IncompatibleTypeError> + Send + Sync,
        >(
            &self,
            key: S,
            value: V,
        ) -> increment::Builder<'_, Self, V> {
            increment::Builder::new(
                self,
                self.key_namespace().map(Into::into),
                false,
                key.into(),
                value.into(),
            )
        }

        /// Gets the value stored at `key`. This function returns a builder that is also a
        /// Future. Awaiting the builder will execute [`Command::Get`] with the options
        /// given.
        fn get_key<S: Into<String>>(&'_ self, key: S) -> get::Builder<'_, Self> {
            get::Builder::new(self, self.key_namespace().map(Into::into), key.into())
        }

        /// Deletes the value stored at `key`.
        fn delete_key<S: Into<String> + Send>(&'_ self, key: S) -> Result<KeyStatus, Error> {
            match self.execute_key_operation(KeyOperation {
                namespace: self.key_namespace().map(ToOwned::to_owned),
                key: key.into(),
                command: Command::Delete,
            })? {
                Output::Status(status) => Ok(status),
                Output::Value(_) => unreachable!("invalid output from delete operation"),
            }
        }

        /// The current namespace.
        fn key_namespace(&self) -> Option<&'_ str> {
            None
        }

        /// Access this Key-Value store within a namespace. When using the returned
        /// [`Namespaced`] instance, all keys specified will be separated into their
        /// own storage designated by `namespace`.
        fn with_key_namespace(&'_ self, namespace: &str) -> Namespaced<'_, Self> {
            Namespaced::new(namespace.to_string(), self)
        }
    }

    /// Key-Value store methods. The Key-Value store is designed to be a
    /// high-performance, lightweight storage mechanism.
    ///
    /// When compared to Collections, the Key-Value store does not offer
    /// ACID-compliant transactions. Instead, the Key-Value store is made more
    /// efficient by periodically flushing the store to disk rather than during
    /// each operation. As such, the Key-Value store is intended to be used as a
    /// lightweight caching layer. However, because each of the operations it
    /// supports are executed atomically, the Key-Value store can also be
    /// utilized for synchronized locking.
    ///
    /// ## Floating Point Operations
    ///
    /// When using [`KeyValue::set_numeric_key()`] or any numeric operations, if
    /// a [Not a Number (NaN) value][nan] is encountered, [`Error::NotANumber`]
    /// will be returned without allowing the operation to succeed.
    ///
    /// Positive and negative infinity values are allowed, as they do not break
    /// comparison operations.
    ///
    /// [nan]: https://en.wikipedia.org/wiki/NaN
    #[async_trait]
    pub trait AsyncKeyValue: Sized + Send + Sync {
        /// Executes a single [`KeyOperation`].
        async fn execute_key_operation(&self, op: KeyOperation) -> Result<Output, Error>;

        /// Sets `key` to `value`. This function returns a builder that is also a
        /// Future. Awaiting the builder will execute [`Command::Set`] with the options
        /// given.
        fn set_key<'a, S: Into<String>, V: Serialize + Send + Sync>(
            &'a self,
            key: S,
            value: &'a V,
        ) -> set::AsyncBuilder<'a, Self, V> {
            set::AsyncBuilder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Serializeable(value),
            )
        }

        /// Sets `key` to `bytes`. This function returns a builder that is also
        /// a Future. Awaiting the builder will execute [`Command::Set`] with
        /// the options given.
        fn set_binary_key<'a, S: Into<String>>(
            &'a self,
            key: S,
            bytes: &'a [u8],
        ) -> set::AsyncBuilder<'a, Self, ()> {
            set::AsyncBuilder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Bytes(bytes),
            )
        }

        /// Sets `key` to `value`. This stores the value as a `Numeric`,
        /// enabling atomic math operations to be performed on this key. This
        /// function returns a builder that is also a Future. Awaiting the
        /// builder will execute [`Command::Set`] with the options given.
        fn set_numeric_key<S: Into<String>, V: Into<Numeric>>(
            &self,
            key: S,
            value: V,
        ) -> set::AsyncBuilder<'_, Self, ()> {
            set::AsyncBuilder::new(
                self,
                self.key_namespace().map(Into::into),
                key.into(),
                PendingValue::Numeric(value.into()),
            )
        }

        /// Increments `key` by `value`. The value stored must be a `Numeric`,
        /// otherwise an error will be returned. The result of the increment
        /// will be the `value`'s type. For example, if the stored value is
        /// currently a `u64`, but `value` is a `f64`, the current value will be
        /// converted to an `f64`, and the stored value will be an `f64`.
        fn increment_key_by<
            S: Into<String> + Send + Sync,
            V: Into<Numeric> + TryFrom<Numeric, Error = IncompatibleTypeError> + Send + Sync,
        >(
            &self,
            key: S,
            value: V,
        ) -> increment::AsyncBuilder<'_, Self, V> {
            increment::AsyncBuilder::new(
                self,
                self.key_namespace().map(Into::into),
                true,
                key.into(),
                value.into(),
            )
        }

        /// Decrements `key` by `value`. The value stored must be a `Numeric`,
        /// otherwise an error will be returned. The result of the decrement
        /// will be the `value`'s type. For example, if the stored value is
        /// currently a `u64`, but `value` is a `f64`, the current value will be
        /// converted to an `f64`, and the stored value will be an `f64`.
        fn decrement_key_by<
            S: Into<String> + Send + Sync,
            V: Into<Numeric> + TryFrom<Numeric, Error = IncompatibleTypeError> + Send + Sync,
        >(
            &self,
            key: S,
            value: V,
        ) -> increment::AsyncBuilder<'_, Self, V> {
            increment::AsyncBuilder::new(
                self,
                self.key_namespace().map(Into::into),
                false,
                key.into(),
                value.into(),
            )
        }

        /// Gets the value stored at `key`. This function returns a builder that is also a
        /// Future. Awaiting the builder will execute [`Command::Get`] with the options
        /// given.
        fn get_key<S: Into<String>>(&'_ self, key: S) -> get::AsyncBuilder<'_, Self> {
            get::AsyncBuilder::new(self, self.key_namespace().map(Into::into), key.into())
        }

        /// Deletes the value stored at `key`.
        async fn delete_key<S: Into<String> + Send>(&'_ self, key: S) -> Result<KeyStatus, Error> {
            match self
                .execute_key_operation(KeyOperation {
                    namespace: self.key_namespace().map(ToOwned::to_owned),
                    key: key.into(),
                    command: Command::Delete,
                })
                .await?
            {
                Output::Status(status) => Ok(status),
                Output::Value(_) => unreachable!("invalid output from delete operation"),
            }
        }

        /// The current namespace.
        fn key_namespace(&self) -> Option<&'_ str> {
            None
        }

        /// Access this Key-Value store within a namespace. When using the returned
        /// [`Namespaced`] instance, all keys specified will be separated into their
        /// own storage designated by `namespace`.
        fn with_key_namespace(&'_ self, namespace: &str) -> Namespaced<'_, Self> {
            Namespaced::new(namespace.to_string(), self)
        }
    }

    enum BuilderState<'a, T, V> {
        Pending(Option<T>),
        Executing(BoxFuture<'a, V>),
    }

    #[allow(clippy::redundant_pub_crate)]
    pub(crate) enum PendingValue<'a, V> {
        Bytes(&'a [u8]),
        Serializeable(&'a V),
        Numeric(Numeric),
    }

    impl<'a, V> PendingValue<'a, V>
    where
        V: Serialize,
    {
        fn prepare(self) -> Result<Value, Error> {
            match self {
                Self::Bytes(bytes) => Ok(Value::Bytes(Bytes::from(bytes))),
                Self::Serializeable(value) => Ok(Value::Bytes(Bytes::from(pot::to_vec(value)?))),
                Self::Numeric(numeric) => Ok(Value::Numeric(numeric)),
            }
        }
    }
}

pub use implementation::*;

/// Checks for existing keys.
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
pub enum KeyCheck {
    /// Only allow the operation if an existing key is present.
    OnlyIfPresent,
    /// Only allow the opeartion if the key isn't present.
    OnlyIfVacant,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
/// An operation performed on a key.
pub struct KeyOperation {
    /// The namespace for the key.
    pub namespace: Option<String>,
    /// The key to operate on.
    pub key: String,
    /// The command to execute.
    pub command: Command,
}

/// Commands for a key-value store.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum Command {
    /// Set a key/value pair.
    Set(SetCommand),
    /// Get the value from a key.
    Get {
        /// Remove the key after retrieving the value.
        delete: bool,
    },
    /// Increment a numeric key. Returns an error if the key cannot be
    /// deserialized to the same numeric type as `amount`. If `saturating` is
    /// true, overflows will be prevented and the value will remain within the
    /// numeric bounds.
    Increment {
        /// The amount to increment by.
        amount: Numeric,
        /// If true, the result will be constrained to the numerical bounds of
        /// the type of `amount`.
        saturating: bool,
    },
    /// Decrement a numeric key. Returns an error if the key cannot be
    /// deserialized to the same numeric type as `amount`. If `saturating` is
    /// true, overflows will be prevented and the value will remain within the
    /// numeric bounds.
    Decrement {
        /// The amount to increment by.
        amount: Numeric,
        /// If true, the result will be constrained to the numerical bounds of
        /// the type of `amount`.
        saturating: bool,
    },
    /// Delete a key.
    Delete,
}

/// Set a key/value pair.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct SetCommand {
    /// The value.
    pub value: Value,
    /// If set, the key will be set to expire automatically.
    pub expiration: Option<Timestamp>,
    /// If true and the key already exists, the expiration will not be
    /// updated. If false and an expiration is provided, the expiration will
    /// be set.
    pub keep_existing_expiration: bool,
    /// Conditional checks for whether the key is already present or not.
    pub check: Option<KeyCheck>,
    /// If true and the key already exists, the existing key will be returned if overwritten.
    pub return_previous_value: bool,
}

/// A value stored in a key.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Value {
    /// A value stored as a byte array.
    Bytes(Bytes),
    /// A numeric value.
    Numeric(Numeric),
}

impl Value {
    /// Validates this value to ensure it is safe to store.
    pub fn validate(self) -> Result<Self, Error> {
        match self {
            Self::Numeric(numeric) => numeric.validate().map(Self::Numeric),
            Self::Bytes(vec) => Ok(Self::Bytes(vec)),
        }
    }

    /// Deserializes the bytes contained inside of this value. Returns an error
    /// if this value doesn't contain bytes.
    pub fn deserialize<V: for<'de> Deserialize<'de>>(&self) -> Result<V, Error> {
        match self {
            Self::Bytes(bytes) => Ok(pot::from_slice(bytes)?),
            Self::Numeric(_) => Err(Error::Database(String::from(
                "key contains numeric value, not serialized data",
            ))),
        }
    }

    /// Returns this value as an `i64`, allowing for precision to be lost if the type was not an `i64` originally. If saturating is true, the conversion will not allow overflows. Returns None if the value is bytes.
    #[must_use]
    pub fn as_i64_lossy(&self, saturating: bool) -> Option<i64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => Some(value.as_i64_lossy(saturating)),
        }
    }

    /// Returns this value as an `u64`, allowing for precision to be lost if the type was not an `u64` originally. If saturating is true, the conversion will not allow overflows. Returns None if the value is bytes.
    #[must_use]
    pub fn as_u64_lossy(&self, saturating: bool) -> Option<u64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => Some(value.as_u64_lossy(saturating)),
        }
    }

    /// Returns this value as an `f64`, allowing for precision to be lost if the type was not an `f64` originally. Returns None if the value is bytes.
    #[must_use]
    pub const fn as_f64_lossy(&self) -> Option<f64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => Some(value.as_f64_lossy()),
        }
    }

    /// Returns this numeric as an `i64`, allowing for precision to be lost if the type was not an `i64` originally. Returns None if the value is bytes.
    #[must_use]
    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => value.as_i64(),
        }
    }

    /// Returns this numeric as an `u64`, allowing for precision to be lost if the type was not an `u64` originally. Returns None if the value is bytes.
    #[must_use]
    pub fn as_u64(&self) -> Option<u64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => value.as_u64(),
        }
    }

    /// Returns this numeric as an `f64`, allowing for precision to be lost if the type was not an `f64` originally. Returns None if the value is bytes.
    #[must_use]
    pub const fn as_f64(&self) -> Option<f64> {
        match self {
            Self::Bytes(_) => None,
            Self::Numeric(value) => value.as_f64(),
        }
    }
}

/// A numerical value.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Numeric {
    /// A 64-bit signed integer.
    Integer(i64),
    /// A 64-bit unsigned integer.
    UnsignedInteger(u64),
    /// A 64-bit floating point number.
    Float(f64),
}

impl Numeric {
    /// Ensures this value contains a valid value.
    ///
    /// # Errors
    ///
    /// [`Error::NotANumber`] is returned if this contains a NaN floating point
    /// value.
    pub fn validate(self) -> Result<Self, Error> {
        if let Self::Float(float) = self {
            if float.is_nan() {
                return Err(Error::NotANumber);
            }
        }

        Ok(self)
    }

    /// Returns this numeric as an `i64`. If this conversion cannot be done
    /// without losing precision or overflowing, None will be returned.
    #[must_use]
    #[allow(clippy::cast_possible_truncation)]
    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Self::Integer(value) => Some(*value),
            Self::UnsignedInteger(value) => (*value).try_into().ok(),
            Self::Float(value) => {
                if value.fract().abs() > 0. {
                    None
                } else {
                    Some(*value as i64)
                }
            }
        }
    }

    /// Returns this numeric as an `i64`, allowing for precision to be lost if
    /// the type was not an `i64` originally. If saturating is true, the
    /// conversion will not allow overflows.
    #[must_use]
    #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
    pub fn as_i64_lossy(&self, saturating: bool) -> i64 {
        match self {
            Self::Integer(value) => *value,
            Self::UnsignedInteger(value) => {
                if saturating {
                    (*value).try_into().unwrap_or(i64::MAX)
                } else {
                    *value as i64
                }
            }
            Self::Float(value) => *value as i64,
        }
    }

    /// Returns this numeric as an `u64`. If this conversion cannot be done
    /// without losing precision or overflowing, None will be returned.
    #[must_use]
    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
    pub fn as_u64(&self) -> Option<u64> {
        match self {
            Self::UnsignedInteger(value) => Some(*value),
            Self::Integer(value) => (*value).try_into().ok(),
            Self::Float(value) => {
                if value.fract() < f64::EPSILON && value.is_sign_positive() {
                    Some(*value as u64)
                } else {
                    None
                }
            }
        }
    }

    /// Returns this numeric as an `u64`, allowing for precision to be lost if
    /// the type was not an `i64` originally. If saturating is true, the
    /// conversion will not allow overflows.
    #[must_use]
    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
    pub fn as_u64_lossy(&self, saturating: bool) -> u64 {
        match self {
            Self::UnsignedInteger(value) => *value,
            Self::Integer(value) => {
                if saturating {
                    (*value).try_into().unwrap_or(0)
                } else {
                    *value as u64
                }
            }
            Self::Float(value) => *value as u64,
        }
    }

    /// Returns this numeric as an `f64`. If this conversion cannot be done
    /// without losing precision, None will be returned.
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    pub const fn as_f64(&self) -> Option<f64> {
        match self {
            Self::UnsignedInteger(value) => {
                if *value > 2_u64.pow(f64::MANTISSA_DIGITS) {
                    None
                } else {
                    Some(*value as f64)
                }
            }
            Self::Integer(value) => {
                if *value > 2_i64.pow(f64::MANTISSA_DIGITS)
                    || *value < -(2_i64.pow(f64::MANTISSA_DIGITS))
                {
                    None
                } else {
                    Some(*value as f64)
                }
            }
            Self::Float(value) => Some(*value),
        }
    }

    /// Returns this numeric as an `f64`, allowing for precision to be lost if
    /// the type was not an `f64` originally.
    #[must_use]
    #[allow(clippy::cast_precision_loss)]
    pub const fn as_f64_lossy(&self) -> f64 {
        match self {
            Self::UnsignedInteger(value) => *value as f64,
            Self::Integer(value) => *value as f64,
            Self::Float(value) => *value,
        }
    }
}

/// A conversion between numeric types wasn't supported.
#[derive(thiserror::Error, Debug)]
#[error("incompatible numeric type")]
pub struct IncompatibleTypeError;

impl From<f64> for Numeric {
    fn from(value: f64) -> Self {
        Self::Float(value)
    }
}

impl From<i64> for Numeric {
    fn from(value: i64) -> Self {
        Self::Integer(value)
    }
}

impl From<u64> for Numeric {
    fn from(value: u64) -> Self {
        Self::UnsignedInteger(value)
    }
}

#[allow(clippy::fallible_impl_from)]
impl TryFrom<Numeric> for f64 {
    type Error = IncompatibleTypeError;
    fn try_from(value: Numeric) -> Result<Self, IncompatibleTypeError> {
        if let Numeric::Float(value) = value {
            Ok(value)
        } else {
            Err(IncompatibleTypeError)
        }
    }
}

#[allow(clippy::fallible_impl_from)]
impl TryFrom<Numeric> for u64 {
    type Error = IncompatibleTypeError;
    fn try_from(value: Numeric) -> Result<Self, IncompatibleTypeError> {
        if let Numeric::UnsignedInteger(value) = value {
            Ok(value)
        } else {
            Err(IncompatibleTypeError)
        }
    }
}

#[allow(clippy::fallible_impl_from)]
impl TryFrom<Numeric> for i64 {
    type Error = IncompatibleTypeError;
    fn try_from(value: Numeric) -> Result<Self, IncompatibleTypeError> {
        if let Numeric::Integer(value) = value {
            Ok(value)
        } else {
            Err(IncompatibleTypeError)
        }
    }
}

/// The result of a [`KeyOperation`].
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum Output {
    /// A status was returned.
    Status(KeyStatus),
    /// A value was returned.
    Value(Option<Value>),
}
/// The status of an operation on a Key.
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum KeyStatus {
    /// A new key was inserted.
    Inserted,
    /// An existing key was updated with a new value.
    Updated,
    /// A key was deleted.
    Deleted,
    /// No changes were made.
    NotChanged,
}