Skip to main content

commonware_storage/qmdb/immutable/operation/
mod.rs

1//! Operations for immutable authenticated databases.
2//!
3//! This module provides the [Operation] type for databases that only support
4//! adding new keyed values (no updates or deletions).
5//!
6//! The operation type is generic over the value encoding, which determines
7//! whether operations are fixed-size or variable-size on disk.
8
9pub(crate) mod fixed;
10pub(crate) mod variable;
11
12use crate::{
13    merkle::{Family, Location},
14    qmdb::{
15        any::ValueEncoding,
16        operation::{Floored, Key, Operation as OperationTrait},
17    },
18};
19use commonware_codec::Encode;
20use commonware_formatting::hex;
21use core::fmt::Display;
22
23// Context byte prefixes for identifying the operation type.
24pub(crate) const SET_CONTEXT: u8 = 0;
25pub(crate) const COMMIT_CONTEXT: u8 = 1;
26
27/// An operation applied to an immutable authenticated database.
28///
29/// Unlike mutable database operations, immutable operations only support
30/// setting new values and committing - no updates or deletions.
31#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
32pub enum Operation<F: Family, K: Key, V: ValueEncoding> {
33    /// Set a key to a value. The key must be set at most once across the database history.
34    Set(K, V::Value),
35
36    /// Commit with optional metadata and the inactivity floor location.
37    /// Operations before the floor are declared inactive by the application.
38    Commit(Option<V::Value>, Location<F>),
39}
40
41impl<F: Family, K: Key, V: ValueEncoding> Operation<F, K, V> {
42    /// If this is an operation involving a key, returns the key. Otherwise, returns None.
43    pub const fn key(&self) -> Option<&K> {
44        match self {
45            Self::Set(key, _) => Some(key),
46            Self::Commit(_, _) => None,
47        }
48    }
49
50    /// Returns true if this is a commit operation.
51    pub const fn is_commit(&self) -> bool {
52        matches!(self, Self::Commit(_, _))
53    }
54
55    /// Returns the inactivity floor location if this is a commit operation.
56    pub const fn has_floor(&self) -> Option<Location<F>> {
57        match self {
58            Self::Commit(_, loc) => Some(*loc),
59            Self::Set(_, _) => None,
60        }
61    }
62}
63
64impl<F: Family, K: Key, V: ValueEncoding> OperationTrait<F> for Operation<F, K, V> {
65    type Key = K;
66
67    fn key(&self) -> Option<&Self::Key> {
68        self.key()
69    }
70
71    fn into_key(self) -> Option<Self::Key> {
72        match self {
73            Self::Set(key, _) => Some(key),
74            Self::Commit(_, _) => None,
75        }
76    }
77
78    fn is_delete(&self) -> bool {
79        // Immutable databases don't support deletion
80        false
81    }
82
83    fn is_update(&self) -> bool {
84        matches!(self, Self::Set(_, _))
85    }
86}
87
88impl<F: Family, K: Key, V: ValueEncoding> Floored<F> for Operation<F, K, V> {
89    fn has_floor(&self) -> Option<Location<F>> {
90        self.has_floor()
91    }
92}
93
94impl<F: Family, K: Key, V: ValueEncoding> Display for Operation<F, K, V> {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Set(key, value) => {
98                write!(f, "[key:{} value:{}]", hex(key), hex(&value.encode()))
99            }
100            Self::Commit(value, floor) => {
101                if let Some(value) = value {
102                    write!(f, "[commit {} floor:{}]", hex(&value.encode()), **floor)
103                } else {
104                    write!(f, "[commit floor:{}]", **floor)
105                }
106            }
107        }
108    }
109}
110
111#[cfg(feature = "arbitrary")]
112impl<F: Family, K: Key, V: ValueEncoding> arbitrary::Arbitrary<'_> for Operation<F, K, V>
113where
114    K: for<'a> arbitrary::Arbitrary<'a>,
115    V::Value: for<'a> arbitrary::Arbitrary<'a>,
116{
117    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
118        let choice = u.int_in_range(0..=1)?;
119        match choice {
120            0 => {
121                let key = K::arbitrary(u)?;
122                let value = V::Value::arbitrary(u)?;
123                Ok(Self::Set(key, value))
124            }
125            1 => {
126                let metadata = Option::<V::Value>::arbitrary(u)?;
127                let max_loc = F::MAX_LEAVES;
128                let floor = u.int_in_range(0..=*max_loc)?;
129                Ok(Self::Commit(metadata, Location::new(floor)))
130            }
131            _ => unreachable!(),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::{merkle::mmr, qmdb::any::value::VariableEncoding};
140    use commonware_codec::Encode;
141    use commonware_utils::sequence::U64;
142
143    type VarOp = Operation<mmr::Family, U64, VariableEncoding<U64>>;
144
145    #[test]
146    fn test_operation_key() {
147        let key = U64::new(1234);
148        let value = U64::new(56789);
149
150        let set_op = VarOp::Set(key.clone(), value.clone());
151        assert_eq!(&key, set_op.key().unwrap());
152
153        let commit_op = VarOp::Commit(Some(value), Location::new(0));
154        assert_eq!(None, commit_op.key());
155
156        let commit_op_none = VarOp::Commit(None, Location::new(0));
157        assert_eq!(None, commit_op_none.key());
158    }
159
160    #[test]
161    fn test_operation_is_commit() {
162        let key = U64::new(1234);
163        let value = U64::new(56789);
164
165        let set_op = VarOp::Set(key, value.clone());
166        assert!(!set_op.is_commit());
167
168        let commit_op = VarOp::Commit(Some(value), Location::new(0));
169        assert!(commit_op.is_commit());
170
171        let commit_op_none = VarOp::Commit(None, Location::new(0));
172        assert!(commit_op_none.is_commit());
173    }
174
175    #[test]
176    fn test_operation_has_floor() {
177        let key = U64::new(1234);
178        let value = U64::new(56789);
179
180        let set_op = VarOp::Set(key, value.clone());
181        assert_eq!(<VarOp as Floored<mmr::Family>>::has_floor(&set_op), None);
182
183        let commit_op = VarOp::Commit(Some(value), Location::new(42));
184        assert_eq!(
185            <VarOp as Floored<mmr::Family>>::has_floor(&commit_op),
186            Some(Location::new(42))
187        );
188
189        let commit_op_none = VarOp::Commit(None, Location::new(0));
190        assert_eq!(
191            <VarOp as Floored<mmr::Family>>::has_floor(&commit_op_none),
192            Some(Location::new(0))
193        );
194    }
195
196    #[test]
197    fn test_operation_display() {
198        let key = U64::new(1234);
199        let value = U64::new(56789);
200
201        let set_op = VarOp::Set(key.clone(), value.clone());
202        assert_eq!(
203            format!("{set_op}"),
204            format!("[key:{} value:{}]", hex(&key), hex(&value.encode()))
205        );
206
207        let commit_op = VarOp::Commit(Some(value.clone()), Location::new(10));
208        assert_eq!(
209            format!("{commit_op}"),
210            format!("[commit {} floor:10]", hex(&value.encode()))
211        );
212
213        let commit_op = VarOp::Commit(None, Location::new(0));
214        assert_eq!(format!("{commit_op}"), "[commit floor:0]");
215    }
216}