Skip to main content

trine_kv/
write_batch.rs

1use crate::{
2    bucket::DEFAULT_BUCKET_NAME,
3    error::{Error, Result},
4    types::{KeyRange, Value},
5};
6
7/// One operation inside an atomic write batch.
8///
9/// Applications usually build these through [`WriteBatch`] methods instead of
10/// constructing variants directly. The variants are public so callers can
11/// inspect a batch before passing it to [`crate::Db::write_sync`] or
12/// [`crate::Db::write`].
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum BatchOperation {
15    /// Insert or replace one key/value pair.
16    Put {
17        /// Target bucket name.
18        bucket: String,
19        /// User key bytes.
20        key: Vec<u8>,
21        /// Value bytes to store.
22        value: Value,
23    },
24    /// Delete one key.
25    Delete {
26        /// Target bucket name.
27        bucket: String,
28        /// User key bytes.
29        key: Vec<u8>,
30    },
31    /// Delete all visible keys in a range.
32    DeleteRange {
33        /// Target bucket name.
34        bucket: String,
35        /// User-key range to delete.
36        range: KeyRange,
37    },
38}
39
40impl BatchOperation {
41    /// Returns the bucket targeted by this operation.
42    #[must_use]
43    pub fn bucket(&self) -> &str {
44        match self {
45            Self::Put { bucket, .. }
46            | Self::Delete { bucket, .. }
47            | Self::DeleteRange { bucket, .. } => bucket,
48        }
49    }
50}
51
52/// Atomic group of writes that may span multiple buckets.
53///
54/// Methods without a bucket suffix target the built-in default bucket. Methods
55/// ending in `_bucket` target an optional named bucket returned by `Db::bucket`.
56///
57/// A batch is committed with [`crate::Db::write_sync`] or [`crate::Db::write`].
58/// Trine assigns one commit sequence to the entire batch, appends the accepted
59/// operations to the WAL for persistent databases, and publishes the batch to
60/// the affected memtables atomically from the caller's point of view.
61///
62/// # Examples
63///
64/// ```rust
65/// use trine_kv::{Db, WriteBatch, WriteOptions};
66///
67/// # fn main() -> trine_kv::Result<()> {
68/// let db = Db::open_sync(trine_kv::DbOptions::memory())?;
69/// let users = db.bucket_sync("users")?;
70///
71/// let mut batch = WriteBatch::new();
72/// batch.put(b"system:ready", b"yes");
73/// batch.put_bucket(users.name().as_str(), b"1", b"Ada")?;
74///
75/// let commit = db.write_sync(batch, WriteOptions::sync_all())?;
76/// assert!(commit.sequence().get() > 0);
77/// # Ok(())
78/// # }
79/// ```
80#[derive(Debug, Clone, Default, PartialEq, Eq)]
81pub struct WriteBatch {
82    operations: Vec<BatchOperation>,
83}
84
85impl WriteBatch {
86    /// Creates an empty batch.
87    ///
88    /// The batch does not reserve a commit sequence until it is passed to a
89    /// database write method.
90    #[must_use]
91    pub const fn new() -> Self {
92        Self {
93            operations: Vec::new(),
94        }
95    }
96
97    /// Adds a key/value write to the default bucket.
98    ///
99    /// # Parameters
100    ///
101    /// - `key`: user key bytes for the built-in default bucket.
102    /// - `value`: value bytes to store.
103    pub fn put(&mut self, key: impl Into<Vec<u8>>, value: impl Into<Value>) {
104        self.operations.push(BatchOperation::Put {
105            bucket: DEFAULT_BUCKET_NAME.to_owned(),
106            key: key.into(),
107            value: value.into(),
108        });
109    }
110
111    /// Adds a key/value write for a named bucket.
112    ///
113    /// The bucket name must refer to an optional named bucket. Use
114    /// [`WriteBatch::put`] for the built-in default bucket.
115    ///
116    /// # Parameters
117    ///
118    /// - `bucket`: target named bucket.
119    /// - `key`: user key bytes.
120    /// - `value`: value bytes to store.
121    ///
122    /// # Errors
123    ///
124    /// Returns [`Error::InvalidOptions`] if `bucket` is empty or is the reserved
125    /// default bucket name.
126    pub fn put_bucket(
127        &mut self,
128        bucket: impl Into<String>,
129        key: impl Into<Vec<u8>>,
130        value: impl Into<Value>,
131    ) -> Result<()> {
132        let bucket = bucket.into();
133        validate_named_bucket(&bucket)?;
134        self.operations.push(BatchOperation::Put {
135            bucket,
136            key: key.into(),
137            value: value.into(),
138        });
139        Ok(())
140    }
141
142    /// Adds a point delete to the default bucket.
143    ///
144    /// The delete hides older values for the same key after the batch commits.
145    /// Snapshots older than the commit sequence can still see earlier values.
146    pub fn delete(&mut self, key: impl Into<Vec<u8>>) {
147        self.operations.push(BatchOperation::Delete {
148            bucket: DEFAULT_BUCKET_NAME.to_owned(),
149            key: key.into(),
150        });
151    }
152
153    /// Adds a point delete for a named bucket.
154    pub fn delete_bucket(
155        &mut self,
156        bucket: impl Into<String>,
157        key: impl Into<Vec<u8>>,
158    ) -> Result<()> {
159        let bucket = bucket.into();
160        validate_named_bucket(&bucket)?;
161        self.operations.push(BatchOperation::Delete {
162            bucket,
163            key: key.into(),
164        });
165        Ok(())
166    }
167
168    /// Adds a range delete to the default bucket.
169    ///
170    /// The delete hides all keys in `range` for read sequences after the batch
171    /// commits. The operation is stored as a range tombstone and can conflict
172    /// with optimistic transactions that read overlapping keys or ranges.
173    pub fn delete_range(&mut self, range: KeyRange) {
174        self.operations.push(BatchOperation::DeleteRange {
175            bucket: DEFAULT_BUCKET_NAME.to_owned(),
176            range,
177        });
178    }
179
180    /// Adds a range delete for a named bucket.
181    pub fn delete_range_bucket(
182        &mut self,
183        bucket: impl Into<String>,
184        range: KeyRange,
185    ) -> Result<()> {
186        let bucket = bucket.into();
187        validate_named_bucket(&bucket)?;
188        self.operations
189            .push(BatchOperation::DeleteRange { bucket, range });
190        Ok(())
191    }
192
193    /// Returns the operations in insertion order.
194    #[must_use]
195    pub fn operations(&self) -> &[BatchOperation] {
196        &self.operations
197    }
198
199    /// Consumes the batch and returns its operations in insertion order.
200    #[must_use]
201    pub fn into_operations(self) -> Vec<BatchOperation> {
202        self.operations
203    }
204
205    /// Returns the number of operations in the batch.
206    #[must_use]
207    pub fn len(&self) -> usize {
208        self.operations.len()
209    }
210
211    /// Returns `true` when the batch contains no operations.
212    #[must_use]
213    pub fn is_empty(&self) -> bool {
214        self.operations.is_empty()
215    }
216}
217
218fn validate_named_bucket(bucket: &str) -> Result<()> {
219    if bucket.is_empty() {
220        return Err(Error::invalid_options("bucket name cannot be empty"));
221    }
222    if bucket == DEFAULT_BUCKET_NAME {
223        return Err(Error::invalid_options(
224            "default bucket writes use default batch methods",
225        ));
226    }
227    Ok(())
228}