slatedb 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,11 @@ use std::cell::RefCell;
2
2
 
3
3
  use magnus::prelude::*;
4
4
  use magnus::{method, Error, RHash, Ruby};
5
- use slatedb::config::{DurabilityLevel, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions};
6
- use slatedb::DBTransaction;
5
+ use slatedb::config::{
6
+ DurabilityLevel, MergeOptions, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions,
7
+ };
8
+ use slatedb::DbTransaction;
9
+ use slatedb::IterationOrder;
7
10
 
8
11
  use crate::errors::{closed_error, invalid_argument_error, map_error};
9
12
  use crate::iterator::Iterator;
@@ -16,12 +19,12 @@ use crate::utils::get_optional;
16
19
  /// After commit or rollback, the transaction is closed.
17
20
  #[magnus::wrap(class = "SlateDb::Transaction", free_immediately, size)]
18
21
  pub struct Transaction {
19
- inner: RefCell<Option<DBTransaction>>,
22
+ inner: RefCell<Option<DbTransaction>>,
20
23
  }
21
24
 
22
25
  impl Transaction {
23
- /// Create a new Transaction from a DBTransaction.
24
- pub fn new(txn: DBTransaction) -> Self {
26
+ /// Create a new Transaction from a DbTransaction.
27
+ pub fn new(txn: DbTransaction) -> Self {
25
28
  Self {
26
29
  inner: RefCell::new(Some(txn)),
27
30
  }
@@ -67,13 +70,16 @@ impl Transaction {
67
70
  opts.dirty = dirty;
68
71
  }
69
72
 
73
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
74
+ opts.cache_blocks = cb;
75
+ }
76
+
70
77
  let guard = self.inner.borrow();
71
78
  let txn = guard
72
79
  .as_ref()
73
80
  .ok_or_else(|| closed_error("transaction is closed"))?;
74
81
 
75
- let result =
76
- block_on_result(async { txn.get_with_options(key.as_bytes(), &opts).await })?;
82
+ let result = block_on_result(async { txn.get_with_options(key.as_bytes(), &opts).await })?;
77
83
  Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
78
84
  }
79
85
 
@@ -135,6 +141,53 @@ impl Transaction {
135
141
  Ok(())
136
142
  }
137
143
 
144
+ /// Merge a value within the transaction.
145
+ pub fn merge(&self, key: String, value: String) -> Result<(), Error> {
146
+ if key.is_empty() {
147
+ return Err(invalid_argument_error("key cannot be empty"));
148
+ }
149
+
150
+ let guard = self.inner.borrow();
151
+ let txn = guard
152
+ .as_ref()
153
+ .ok_or_else(|| closed_error("transaction is closed"))?;
154
+
155
+ txn.merge(key.as_bytes(), value.as_bytes())
156
+ .map_err(map_error)?;
157
+
158
+ Ok(())
159
+ }
160
+
161
+ /// Merge a value with options within the transaction.
162
+ pub fn merge_with_options(
163
+ &self,
164
+ key: String,
165
+ value: String,
166
+ kwargs: RHash,
167
+ ) -> Result<(), Error> {
168
+ if key.is_empty() {
169
+ return Err(invalid_argument_error("key cannot be empty"));
170
+ }
171
+
172
+ let ttl = get_optional::<u64>(&kwargs, "ttl")?;
173
+ let merge_opts = MergeOptions {
174
+ ttl: match ttl {
175
+ Some(ms) => Ttl::ExpireAfter(ms),
176
+ None => Ttl::Default,
177
+ },
178
+ };
179
+
180
+ let guard = self.inner.borrow();
181
+ let txn = guard
182
+ .as_ref()
183
+ .ok_or_else(|| closed_error("transaction is closed"))?;
184
+
185
+ txn.merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts)
186
+ .map_err(map_error)?;
187
+
188
+ Ok(())
189
+ }
190
+
138
191
  /// Scan a range of keys within the transaction.
139
192
  pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
140
193
  if start.is_empty() {
@@ -200,6 +253,18 @@ impl Transaction {
200
253
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
201
254
  opts.max_fetch_tasks = mft;
202
255
  }
256
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
257
+ opts.order = match order.as_str() {
258
+ "ascending" | "asc" => IterationOrder::Ascending,
259
+ "descending" | "desc" => IterationOrder::Descending,
260
+ other => {
261
+ return Err(invalid_argument_error(&format!(
262
+ "invalid order: {} (expected 'asc' or 'desc')",
263
+ other
264
+ )))
265
+ }
266
+ };
267
+ }
203
268
 
204
269
  let guard = self.inner.borrow();
205
270
  let txn = guard
@@ -219,6 +284,106 @@ impl Transaction {
219
284
  Ok(Iterator::new(iter))
220
285
  }
221
286
 
287
+ /// Scan all keys with a given prefix within the transaction.
288
+ pub fn scan_prefix(&self, prefix: String) -> Result<Iterator, Error> {
289
+ if prefix.is_empty() {
290
+ return Err(invalid_argument_error("prefix cannot be empty"));
291
+ }
292
+
293
+ let guard = self.inner.borrow();
294
+ let txn = guard
295
+ .as_ref()
296
+ .ok_or_else(|| closed_error("transaction is closed"))?;
297
+
298
+ let iter = block_on_result(async { txn.scan_prefix(prefix.as_bytes()).await })?;
299
+
300
+ Ok(Iterator::new(iter))
301
+ }
302
+
303
+ /// Scan all keys with a given prefix with options within the transaction.
304
+ pub fn scan_prefix_with_options(
305
+ &self,
306
+ prefix: String,
307
+ kwargs: RHash,
308
+ ) -> Result<Iterator, Error> {
309
+ if prefix.is_empty() {
310
+ return Err(invalid_argument_error("prefix cannot be empty"));
311
+ }
312
+
313
+ let mut opts = ScanOptions::default();
314
+
315
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
316
+ opts.durability_filter = match df.as_str() {
317
+ "remote" => DurabilityLevel::Remote,
318
+ "memory" => DurabilityLevel::Memory,
319
+ other => {
320
+ return Err(invalid_argument_error(&format!(
321
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
322
+ other
323
+ )))
324
+ }
325
+ };
326
+ }
327
+
328
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
329
+ opts.dirty = dirty;
330
+ }
331
+
332
+ if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
333
+ opts.read_ahead_bytes = rab;
334
+ }
335
+
336
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
337
+ opts.cache_blocks = cb;
338
+ }
339
+
340
+ if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
341
+ opts.max_fetch_tasks = mft;
342
+ }
343
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
344
+ opts.order = match order.as_str() {
345
+ "ascending" | "asc" => IterationOrder::Ascending,
346
+ "descending" | "desc" => IterationOrder::Descending,
347
+ other => {
348
+ return Err(invalid_argument_error(&format!(
349
+ "invalid order: {} (expected 'asc' or 'desc')",
350
+ other
351
+ )))
352
+ }
353
+ };
354
+ }
355
+
356
+ let guard = self.inner.borrow();
357
+ let txn = guard
358
+ .as_ref()
359
+ .ok_or_else(|| closed_error("transaction is closed"))?;
360
+
361
+ let iter = block_on_result(async {
362
+ txn.scan_prefix_with_options(prefix.as_bytes(), &opts).await
363
+ })?;
364
+
365
+ Ok(Iterator::new(iter))
366
+ }
367
+
368
+ /// Mark keys as read for conflict detection.
369
+ ///
370
+ /// This explicitly tracks reads for conflict checking in serializable isolation,
371
+ /// even when the keys weren't actually read via get().
372
+ ///
373
+ /// # Arguments
374
+ /// * `keys` - Array of keys to mark as read
375
+ pub fn mark_read(&self, keys: Vec<String>) -> Result<(), Error> {
376
+ let guard = self.inner.borrow();
377
+ let txn = guard
378
+ .as_ref()
379
+ .ok_or_else(|| closed_error("transaction is closed"))?;
380
+
381
+ let key_bytes: Vec<&[u8]> = keys.iter().map(|k| k.as_bytes()).collect();
382
+ txn.mark_read(&key_bytes).map_err(map_error)?;
383
+
384
+ Ok(())
385
+ }
386
+
222
387
  /// Commit the transaction.
223
388
  pub fn commit(&self) -> Result<(), Error> {
224
389
  let txn = self
@@ -234,7 +399,11 @@ impl Transaction {
234
399
  /// Commit the transaction with options.
235
400
  pub fn commit_with_options(&self, kwargs: RHash) -> Result<(), Error> {
236
401
  let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
237
- let write_opts = WriteOptions { await_durable };
402
+ let seqnum = get_optional::<u64>(&kwargs, "seqnum")?.unwrap_or(0);
403
+ let write_opts = WriteOptions {
404
+ await_durable,
405
+ seqnum,
406
+ };
238
407
 
239
408
  let txn = self
240
409
  .inner
@@ -275,12 +444,23 @@ pub fn define_transaction_class(ruby: &Ruby, module: &magnus::RModule) -> Result
275
444
  method!(Transaction::put_with_options, 3),
276
445
  )?;
277
446
  class.define_method("_delete", method!(Transaction::delete, 1))?;
447
+ class.define_method("_merge", method!(Transaction::merge, 2))?;
448
+ class.define_method(
449
+ "_merge_with_options",
450
+ method!(Transaction::merge_with_options, 3),
451
+ )?;
278
452
  class.define_method("_scan", method!(Transaction::scan, 2))?;
279
453
  class.define_method(
280
454
  "_scan_with_options",
281
455
  method!(Transaction::scan_with_options, 3),
282
456
  )?;
283
- class.define_method("commit", method!(Transaction::commit, 0))?;
457
+ class.define_method("_scan_prefix", method!(Transaction::scan_prefix, 1))?;
458
+ class.define_method(
459
+ "_scan_prefix_with_options",
460
+ method!(Transaction::scan_prefix_with_options, 2),
461
+ )?;
462
+ class.define_method("_mark_read", method!(Transaction::mark_read, 1))?;
463
+ class.define_method("_commit", method!(Transaction::commit, 0))?;
284
464
  class.define_method(
285
465
  "_commit_with_options",
286
466
  method!(Transaction::commit_with_options, 1),
@@ -2,8 +2,8 @@ use std::sync::Arc;
2
2
 
3
3
  use magnus::value::ReprValue;
4
4
  use magnus::{Error, RHash, Ruby, TryConvert};
5
- use object_store::aws::AmazonS3Builder;
6
- use object_store::ObjectStoreScheme;
5
+ use slatedb::object_store::aws::AmazonS3Builder;
6
+ use slatedb::object_store::{Error as ObjectStoreError, ObjectStore, ObjectStoreScheme};
7
7
  use slatedb::{Db, Error as SlateError};
8
8
  use url::Url;
9
9
 
@@ -24,7 +24,7 @@ pub fn get_optional<T: TryConvert>(hash: &RHash, key: &str) -> Result<Option<T>,
24
24
  }
25
25
 
26
26
  /// Convert an object_store error to a SlateDB error
27
- fn to_slate_error(e: object_store::Error) -> SlateError {
27
+ fn to_slate_error(e: ObjectStoreError) -> SlateError {
28
28
  SlateError::unavailable(e.to_string())
29
29
  }
30
30
 
@@ -33,7 +33,7 @@ fn to_slate_error(e: object_store::Error) -> SlateError {
33
33
  /// This function handles S3 URLs specially to ensure environment variables
34
34
  /// like AWS_ACCESS_KEY_ID are properly recognized (the default object_store
35
35
  /// registry only recognizes lowercase variants like aws_access_key_id).
36
- pub fn resolve_object_store(url: &str) -> Result<Arc<dyn object_store::ObjectStore>, SlateError> {
36
+ pub fn resolve_object_store(url: &str) -> Result<Arc<dyn ObjectStore>, SlateError> {
37
37
  let parsed_url: Url = url
38
38
  .try_into()
39
39
  .map_err(|e: url::ParseError| SlateError::invalid(format!("invalid URL: {}", e)))?;
@@ -44,6 +44,7 @@ pub fn resolve_object_store(url: &str) -> Result<Arc<dyn object_store::ObjectSto
44
44
  match scheme {
45
45
  ObjectStoreScheme::AmazonS3 => {
46
46
  // Use from_env() to properly handle uppercase AWS_* environment variables
47
+ // (the default object_store registry only recognizes lowercase variants)
47
48
  let store = AmazonS3Builder::from_env()
48
49
  .with_url(url)
49
50
  .build()
@@ -2,7 +2,7 @@ use std::cell::RefCell;
2
2
 
3
3
  use magnus::prelude::*;
4
4
  use magnus::{function, method, Error, RHash, Ruby};
5
- use slatedb::config::{PutOptions, Ttl};
5
+ use slatedb::config::{MergeOptions, PutOptions, Ttl};
6
6
  use slatedb::WriteBatch as SlateWriteBatch;
7
7
 
8
8
  use crate::errors::invalid_argument_error;
@@ -72,6 +72,48 @@ impl WriteBatch {
72
72
  Ok(())
73
73
  }
74
74
 
75
+ /// Add a merge operation to the batch.
76
+ pub fn merge(&self, key: String, value: String) -> Result<(), Error> {
77
+ if key.is_empty() {
78
+ return Err(invalid_argument_error("key cannot be empty"));
79
+ }
80
+
81
+ self.inner
82
+ .borrow_mut()
83
+ .merge(key.as_bytes(), value.as_bytes());
84
+
85
+ Ok(())
86
+ }
87
+
88
+ /// Add a merge operation with options to the batch.
89
+ ///
90
+ /// Options:
91
+ /// - ttl: Time-to-live in milliseconds
92
+ pub fn merge_with_options(
93
+ &self,
94
+ key: String,
95
+ value: String,
96
+ kwargs: RHash,
97
+ ) -> Result<(), Error> {
98
+ if key.is_empty() {
99
+ return Err(invalid_argument_error("key cannot be empty"));
100
+ }
101
+
102
+ let ttl = get_optional::<u64>(&kwargs, "ttl")?;
103
+ let merge_opts = MergeOptions {
104
+ ttl: match ttl {
105
+ Some(ms) => Ttl::ExpireAfter(ms),
106
+ None => Ttl::Default,
107
+ },
108
+ };
109
+
110
+ self.inner
111
+ .borrow_mut()
112
+ .merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts);
113
+
114
+ Ok(())
115
+ }
116
+
75
117
  /// Take ownership of the inner WriteBatch (consumes it).
76
118
  /// Used internally when writing the batch to the database.
77
119
  pub fn take(&self) -> Result<SlateWriteBatch, Error> {
@@ -93,6 +135,11 @@ pub fn define_write_batch_class(ruby: &Ruby, module: &magnus::RModule) -> Result
93
135
  method!(WriteBatch::put_with_options, 3),
94
136
  )?;
95
137
  class.define_method("_delete", method!(WriteBatch::delete, 1))?;
138
+ class.define_method("_merge", method!(WriteBatch::merge, 2))?;
139
+ class.define_method(
140
+ "_merge_with_options",
141
+ method!(WriteBatch::merge_with_options, 3),
142
+ )?;
96
143
 
97
144
  Ok(())
98
145
  }