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}