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.
- checksums.yaml +4 -4
- data/README.md +219 -4
- data/ext/slatedb/Cargo.toml +10 -10
- data/ext/slatedb/src/admin.rs +20 -5
- data/ext/slatedb/src/database.rs +376 -39
- data/ext/slatedb/src/iterator.rs +4 -1
- data/ext/slatedb/src/lib.rs +3 -0
- data/ext/slatedb/src/merge_ops.rs +240 -0
- data/ext/slatedb/src/metrics.rs +47 -0
- data/ext/slatedb/src/reader.rs +118 -7
- data/ext/slatedb/src/runtime.rs +52 -1
- data/ext/slatedb/src/snapshot.rs +105 -0
- data/ext/slatedb/src/transaction.rs +189 -9
- data/ext/slatedb/src/utils.rs +5 -4
- data/ext/slatedb/src/write_batch.rs +48 -1
- data/lib/slatedb/database.rb +205 -19
- data/lib/slatedb/metrics.rb +20 -0
- data/lib/slatedb/reader.rb +50 -1
- data/lib/slatedb/snapshot.rb +32 -0
- data/lib/slatedb/transaction.rb +96 -0
- data/lib/slatedb/version.rb +1 -1
- data/lib/slatedb/write_batch.rb +20 -0
- data/lib/slatedb.rb +1 -0
- metadata +16 -13
|
@@ -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::{
|
|
6
|
-
|
|
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<
|
|
22
|
+
inner: RefCell<Option<DbTransaction>>,
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
impl Transaction {
|
|
23
|
-
/// Create a new Transaction from a
|
|
24
|
-
pub fn new(txn:
|
|
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
|
|
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("
|
|
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),
|
data/ext/slatedb/src/utils.rs
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
}
|