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
data/ext/slatedb/src/database.rs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
use std::
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::sync::{Arc, Mutex};
|
|
2
3
|
|
|
3
4
|
use magnus::prelude::*;
|
|
4
5
|
use magnus::{function, method, Error, RHash, Ruby};
|
|
5
|
-
use slatedb::config::{
|
|
6
|
+
use slatedb::config::{
|
|
7
|
+
DurabilityLevel, MergeOptions, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions,
|
|
8
|
+
};
|
|
6
9
|
use slatedb::object_store::memory::InMemory;
|
|
7
|
-
use slatedb::{Db, IsolationLevel};
|
|
10
|
+
use slatedb::{Db, IsolationLevel, IterationOrder, KeyValue};
|
|
8
11
|
|
|
9
12
|
use crate::errors::invalid_argument_error;
|
|
10
13
|
use crate::iterator::Iterator;
|
|
14
|
+
use crate::merge_ops::{parse_merge_operator, parse_merge_operator_proc};
|
|
15
|
+
use crate::metrics::Metrics;
|
|
11
16
|
use crate::runtime::block_on_result;
|
|
12
17
|
use crate::snapshot::Snapshot;
|
|
13
18
|
use crate::transaction::Transaction;
|
|
@@ -20,30 +25,101 @@ use crate::write_batch::WriteBatch;
|
|
|
20
25
|
#[magnus::wrap(class = "SlateDb::Database", free_immediately, size)]
|
|
21
26
|
pub struct Database {
|
|
22
27
|
inner: Arc<Db>,
|
|
28
|
+
metrics: Arc<Mutex<HashMap<String, i64>>>,
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
impl Database {
|
|
32
|
+
fn increment_metric(&self, name: &str) {
|
|
33
|
+
let mut metrics = self.metrics.lock().expect("metrics mutex poisoned");
|
|
34
|
+
*metrics.entry(name.to_string()).or_insert(0) += 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn key_value_to_hash(kv: KeyValue) -> Result<RHash, Error> {
|
|
38
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
39
|
+
let hash = ruby.hash_new();
|
|
40
|
+
hash.aset(
|
|
41
|
+
ruby.to_symbol("key"),
|
|
42
|
+
String::from_utf8_lossy(&kv.key).to_string(),
|
|
43
|
+
)?;
|
|
44
|
+
hash.aset(
|
|
45
|
+
ruby.to_symbol("value"),
|
|
46
|
+
String::from_utf8_lossy(&kv.value).to_string(),
|
|
47
|
+
)?;
|
|
48
|
+
hash.aset(ruby.to_symbol("seq"), kv.seq)?;
|
|
49
|
+
hash.aset(ruby.to_symbol("create_ts"), kv.create_ts)?;
|
|
50
|
+
hash.aset(ruby.to_symbol("expire_ts"), kv.expire_ts)?;
|
|
51
|
+
Ok(hash)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn read_options_from_kwargs(kwargs: &RHash) -> Result<ReadOptions, Error> {
|
|
55
|
+
let mut opts = ReadOptions::default();
|
|
56
|
+
|
|
57
|
+
if let Some(df) = get_optional::<String>(kwargs, "durability_filter")? {
|
|
58
|
+
opts.durability_filter = match df.as_str() {
|
|
59
|
+
"remote" => DurabilityLevel::Remote,
|
|
60
|
+
"memory" => DurabilityLevel::Memory,
|
|
61
|
+
other => {
|
|
62
|
+
return Err(invalid_argument_error(&format!(
|
|
63
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
64
|
+
other
|
|
65
|
+
)))
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if let Some(dirty) = get_optional::<bool>(kwargs, "dirty")? {
|
|
71
|
+
opts.dirty = dirty;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if let Some(cb) = get_optional::<bool>(kwargs, "cache_blocks")? {
|
|
75
|
+
opts.cache_blocks = cb;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Ok(opts)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn write_options_from_kwargs(kwargs: &RHash) -> Result<WriteOptions, Error> {
|
|
82
|
+
let await_durable = get_optional::<bool>(kwargs, "await_durable")?.unwrap_or(true);
|
|
83
|
+
let seqnum = get_optional::<u64>(kwargs, "seqnum")?.unwrap_or(0);
|
|
84
|
+
|
|
85
|
+
Ok(WriteOptions {
|
|
86
|
+
await_durable,
|
|
87
|
+
seqnum,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
26
91
|
/// Open a database at the given path.
|
|
27
92
|
///
|
|
28
93
|
/// # Arguments
|
|
29
94
|
/// * `path` - The path identifier for the database
|
|
30
95
|
/// * `url` - Optional object store URL (e.g., "s3://bucket/path")
|
|
96
|
+
/// * `kwargs` - Additional options (merge_operator, merge_operator_proc)
|
|
31
97
|
///
|
|
32
98
|
/// # Returns
|
|
33
99
|
/// A new Database instance
|
|
34
|
-
pub fn open(path: String, url: Option<String
|
|
100
|
+
pub fn open(path: String, url: Option<String>, kwargs: RHash) -> Result<Self, Error> {
|
|
101
|
+
// Try string-based merge operator first, then proc-based
|
|
102
|
+
let merge_operator = parse_merge_operator(&kwargs)?.or(parse_merge_operator_proc(&kwargs)?);
|
|
103
|
+
|
|
35
104
|
let db = block_on_result(async {
|
|
36
|
-
let object_store: Arc<dyn object_store::ObjectStore> =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
105
|
+
let object_store: Arc<dyn slatedb::object_store::ObjectStore> =
|
|
106
|
+
if let Some(ref url_str) = url {
|
|
107
|
+
resolve_object_store(url_str)?
|
|
108
|
+
} else {
|
|
109
|
+
Arc::new(InMemory::new())
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let mut builder = Db::builder(path, object_store);
|
|
113
|
+
if let Some(merge_operator) = merge_operator {
|
|
114
|
+
builder = builder.with_merge_operator(merge_operator);
|
|
115
|
+
}
|
|
41
116
|
|
|
42
|
-
|
|
117
|
+
builder.build().await
|
|
43
118
|
})?;
|
|
44
119
|
|
|
45
120
|
Ok(Self {
|
|
46
121
|
inner: Arc::new(db),
|
|
122
|
+
metrics: Arc::new(Mutex::new(HashMap::new())),
|
|
47
123
|
})
|
|
48
124
|
}
|
|
49
125
|
|
|
@@ -63,6 +139,7 @@ impl Database {
|
|
|
63
139
|
|
|
64
140
|
let result =
|
|
65
141
|
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
142
|
+
self.increment_metric("db.get.count");
|
|
66
143
|
|
|
67
144
|
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
68
145
|
}
|
|
@@ -80,31 +157,64 @@ impl Database {
|
|
|
80
157
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
81
158
|
}
|
|
82
159
|
|
|
83
|
-
let
|
|
160
|
+
let opts = Self::read_options_from_kwargs(&kwargs)?;
|
|
84
161
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
162
|
+
let result =
|
|
163
|
+
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
164
|
+
self.increment_metric("db.get_with_options.count");
|
|
165
|
+
|
|
166
|
+
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Get a key-value pair with metadata by key.
|
|
170
|
+
///
|
|
171
|
+
/// # Arguments
|
|
172
|
+
/// * `key` - The key to look up
|
|
173
|
+
///
|
|
174
|
+
/// # Returns
|
|
175
|
+
/// A Hash with key, value, seq, create_ts, and expire_ts, or nil if not found
|
|
176
|
+
pub fn get_key_value(&self, key: String) -> Result<Option<RHash>, Error> {
|
|
177
|
+
if key.is_empty() {
|
|
178
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
97
179
|
}
|
|
98
180
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
181
|
+
let opts = ReadOptions::default();
|
|
182
|
+
let result = block_on_result(async {
|
|
183
|
+
self.inner
|
|
184
|
+
.get_key_value_with_options(key.as_bytes(), &opts)
|
|
185
|
+
.await
|
|
186
|
+
})?;
|
|
187
|
+
self.increment_metric("db.get_key_value.count");
|
|
188
|
+
|
|
189
|
+
result.map(Self::key_value_to_hash).transpose()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Get a key-value pair with metadata by key with options.
|
|
193
|
+
///
|
|
194
|
+
/// # Arguments
|
|
195
|
+
/// * `key` - The key to look up
|
|
196
|
+
/// * `kwargs` - Keyword arguments (durability_filter, dirty, cache_blocks)
|
|
197
|
+
///
|
|
198
|
+
/// # Returns
|
|
199
|
+
/// A Hash with key, value, seq, create_ts, and expire_ts, or nil if not found
|
|
200
|
+
pub fn get_key_value_with_options(
|
|
201
|
+
&self,
|
|
202
|
+
key: String,
|
|
203
|
+
kwargs: RHash,
|
|
204
|
+
) -> Result<Option<RHash>, Error> {
|
|
205
|
+
if key.is_empty() {
|
|
206
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
102
207
|
}
|
|
103
208
|
|
|
104
|
-
let
|
|
105
|
-
|
|
209
|
+
let opts = Self::read_options_from_kwargs(&kwargs)?;
|
|
210
|
+
let result = block_on_result(async {
|
|
211
|
+
self.inner
|
|
212
|
+
.get_key_value_with_options(key.as_bytes(), &opts)
|
|
213
|
+
.await
|
|
214
|
+
})?;
|
|
215
|
+
self.increment_metric("db.get_key_value_with_options.count");
|
|
106
216
|
|
|
107
|
-
|
|
217
|
+
result.map(Self::key_value_to_hash).transpose()
|
|
108
218
|
}
|
|
109
219
|
|
|
110
220
|
/// Get a value by key as raw bytes.
|
|
@@ -123,6 +233,7 @@ impl Database {
|
|
|
123
233
|
|
|
124
234
|
let result =
|
|
125
235
|
block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
|
|
236
|
+
self.increment_metric("db.get_bytes.count");
|
|
126
237
|
|
|
127
238
|
Ok(result.map(|b| b.to_vec()))
|
|
128
239
|
}
|
|
@@ -141,6 +252,7 @@ impl Database {
|
|
|
141
252
|
|
|
142
253
|
let write_opts = WriteOptions {
|
|
143
254
|
await_durable: true,
|
|
255
|
+
seqnum: 0,
|
|
144
256
|
};
|
|
145
257
|
|
|
146
258
|
block_on_result(async {
|
|
@@ -148,6 +260,7 @@ impl Database {
|
|
|
148
260
|
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
149
261
|
.await
|
|
150
262
|
})?;
|
|
263
|
+
self.increment_metric("db.put.count");
|
|
151
264
|
|
|
152
265
|
Ok(())
|
|
153
266
|
}
|
|
@@ -157,7 +270,7 @@ impl Database {
|
|
|
157
270
|
/// # Arguments
|
|
158
271
|
/// * `key` - The key to store
|
|
159
272
|
/// * `value` - The value to store
|
|
160
|
-
/// * `kwargs` - Keyword arguments (ttl, await_durable)
|
|
273
|
+
/// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
|
|
161
274
|
pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
|
|
162
275
|
if key.is_empty() {
|
|
163
276
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
@@ -173,14 +286,14 @@ impl Database {
|
|
|
173
286
|
};
|
|
174
287
|
|
|
175
288
|
// Parse await_durable
|
|
176
|
-
let
|
|
177
|
-
let write_opts = WriteOptions { await_durable };
|
|
289
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
178
290
|
|
|
179
291
|
block_on_result(async {
|
|
180
292
|
self.inner
|
|
181
293
|
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
182
294
|
.await
|
|
183
295
|
})?;
|
|
296
|
+
self.increment_metric("db.put_with_options.count");
|
|
184
297
|
|
|
185
298
|
Ok(())
|
|
186
299
|
}
|
|
@@ -196,6 +309,7 @@ impl Database {
|
|
|
196
309
|
|
|
197
310
|
let write_opts = WriteOptions {
|
|
198
311
|
await_durable: true,
|
|
312
|
+
seqnum: 0,
|
|
199
313
|
};
|
|
200
314
|
|
|
201
315
|
block_on_result(async {
|
|
@@ -203,6 +317,7 @@ impl Database {
|
|
|
203
317
|
.delete_with_options(key.as_bytes(), &write_opts)
|
|
204
318
|
.await
|
|
205
319
|
})?;
|
|
320
|
+
self.increment_metric("db.delete.count");
|
|
206
321
|
|
|
207
322
|
Ok(())
|
|
208
323
|
}
|
|
@@ -211,20 +326,20 @@ impl Database {
|
|
|
211
326
|
///
|
|
212
327
|
/// # Arguments
|
|
213
328
|
/// * `key` - The key to delete
|
|
214
|
-
/// * `kwargs` - Keyword arguments (await_durable)
|
|
329
|
+
/// * `kwargs` - Keyword arguments (await_durable, seqnum)
|
|
215
330
|
pub fn delete_with_options(&self, key: String, kwargs: RHash) -> Result<(), Error> {
|
|
216
331
|
if key.is_empty() {
|
|
217
332
|
return Err(invalid_argument_error("key cannot be empty"));
|
|
218
333
|
}
|
|
219
334
|
|
|
220
|
-
let
|
|
221
|
-
let write_opts = WriteOptions { await_durable };
|
|
335
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
222
336
|
|
|
223
337
|
block_on_result(async {
|
|
224
338
|
self.inner
|
|
225
339
|
.delete_with_options(key.as_bytes(), &write_opts)
|
|
226
340
|
.await
|
|
227
341
|
})?;
|
|
342
|
+
self.increment_metric("db.delete_with_options.count");
|
|
228
343
|
|
|
229
344
|
Ok(())
|
|
230
345
|
}
|
|
@@ -311,6 +426,18 @@ impl Database {
|
|
|
311
426
|
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
312
427
|
opts.max_fetch_tasks = mft;
|
|
313
428
|
}
|
|
429
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
430
|
+
opts.order = match order.as_str() {
|
|
431
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
432
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
433
|
+
other => {
|
|
434
|
+
return Err(invalid_argument_error(&format!(
|
|
435
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
436
|
+
other
|
|
437
|
+
)))
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
314
441
|
|
|
315
442
|
let start_bytes = start.into_bytes();
|
|
316
443
|
let end_bytes = end_key.map(|e| e.into_bytes());
|
|
@@ -325,6 +452,97 @@ impl Database {
|
|
|
325
452
|
Ok(Iterator::new(iter))
|
|
326
453
|
}
|
|
327
454
|
|
|
455
|
+
/// Scan all keys with a given prefix.
|
|
456
|
+
///
|
|
457
|
+
/// # Arguments
|
|
458
|
+
/// * `prefix` - The key prefix to scan
|
|
459
|
+
///
|
|
460
|
+
/// # Returns
|
|
461
|
+
/// An Iterator over key-value pairs
|
|
462
|
+
pub fn scan_prefix(&self, prefix: String) -> Result<Iterator, Error> {
|
|
463
|
+
if prefix.is_empty() {
|
|
464
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
let opts = ScanOptions::default();
|
|
468
|
+
let iter = block_on_result(async {
|
|
469
|
+
self.inner
|
|
470
|
+
.scan_prefix_with_options(prefix.as_bytes(), &opts)
|
|
471
|
+
.await
|
|
472
|
+
})?;
|
|
473
|
+
|
|
474
|
+
Ok(Iterator::new(iter))
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// Scan all keys with a given prefix with options.
|
|
478
|
+
///
|
|
479
|
+
/// # Arguments
|
|
480
|
+
/// * `prefix` - The key prefix to scan
|
|
481
|
+
/// * `kwargs` - Keyword arguments (durability_filter, dirty, read_ahead_bytes, cache_blocks, max_fetch_tasks)
|
|
482
|
+
///
|
|
483
|
+
/// # Returns
|
|
484
|
+
/// An Iterator over key-value pairs
|
|
485
|
+
pub fn scan_prefix_with_options(
|
|
486
|
+
&self,
|
|
487
|
+
prefix: String,
|
|
488
|
+
kwargs: RHash,
|
|
489
|
+
) -> Result<Iterator, Error> {
|
|
490
|
+
if prefix.is_empty() {
|
|
491
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let mut opts = ScanOptions::default();
|
|
495
|
+
|
|
496
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
497
|
+
opts.durability_filter = match df.as_str() {
|
|
498
|
+
"remote" => DurabilityLevel::Remote,
|
|
499
|
+
"memory" => DurabilityLevel::Memory,
|
|
500
|
+
other => {
|
|
501
|
+
return Err(invalid_argument_error(&format!(
|
|
502
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
503
|
+
other
|
|
504
|
+
)))
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
510
|
+
opts.dirty = dirty;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
|
|
514
|
+
opts.read_ahead_bytes = rab;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
518
|
+
opts.cache_blocks = cb;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
522
|
+
opts.max_fetch_tasks = mft;
|
|
523
|
+
}
|
|
524
|
+
if let Some(order) = get_optional::<String>(&kwargs, "order")? {
|
|
525
|
+
opts.order = match order.as_str() {
|
|
526
|
+
"ascending" | "asc" => IterationOrder::Ascending,
|
|
527
|
+
"descending" | "desc" => IterationOrder::Descending,
|
|
528
|
+
other => {
|
|
529
|
+
return Err(invalid_argument_error(&format!(
|
|
530
|
+
"invalid order: {} (expected 'asc' or 'desc')",
|
|
531
|
+
other
|
|
532
|
+
)))
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let iter = block_on_result(async {
|
|
538
|
+
self.inner
|
|
539
|
+
.scan_prefix_with_options(prefix.as_bytes(), &opts)
|
|
540
|
+
.await
|
|
541
|
+
})?;
|
|
542
|
+
|
|
543
|
+
Ok(Iterator::new(iter))
|
|
544
|
+
}
|
|
545
|
+
|
|
328
546
|
/// Write a batch of operations atomically.
|
|
329
547
|
///
|
|
330
548
|
/// # Arguments
|
|
@@ -339,10 +557,9 @@ impl Database {
|
|
|
339
557
|
///
|
|
340
558
|
/// # Arguments
|
|
341
559
|
/// * `batch` - The WriteBatch to write
|
|
342
|
-
/// * `kwargs` - Keyword arguments (await_durable)
|
|
560
|
+
/// * `kwargs` - Keyword arguments (await_durable, seqnum)
|
|
343
561
|
pub fn write_with_options(&self, batch: &WriteBatch, kwargs: RHash) -> Result<(), Error> {
|
|
344
|
-
let
|
|
345
|
-
let write_opts = WriteOptions { await_durable };
|
|
562
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
346
563
|
|
|
347
564
|
let batch_inner = batch.take()?;
|
|
348
565
|
|
|
@@ -355,6 +572,67 @@ impl Database {
|
|
|
355
572
|
Ok(())
|
|
356
573
|
}
|
|
357
574
|
|
|
575
|
+
/// Merge a value into the database.
|
|
576
|
+
///
|
|
577
|
+
/// # Arguments
|
|
578
|
+
/// * `key` - The key to merge into
|
|
579
|
+
/// * `value` - The merge operand to apply
|
|
580
|
+
pub fn merge(&self, key: String, value: String) -> Result<(), Error> {
|
|
581
|
+
if key.is_empty() {
|
|
582
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let merge_opts = MergeOptions { ttl: Ttl::Default };
|
|
586
|
+
|
|
587
|
+
let write_opts = WriteOptions {
|
|
588
|
+
await_durable: true,
|
|
589
|
+
seqnum: 0,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
block_on_result(async {
|
|
593
|
+
self.inner
|
|
594
|
+
.merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts, &write_opts)
|
|
595
|
+
.await
|
|
596
|
+
})?;
|
|
597
|
+
|
|
598
|
+
Ok(())
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// Merge a value into the database with options.
|
|
602
|
+
///
|
|
603
|
+
/// # Arguments
|
|
604
|
+
/// * `key` - The key to merge into
|
|
605
|
+
/// * `value` - The merge operand to apply
|
|
606
|
+
/// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
|
|
607
|
+
pub fn merge_with_options(
|
|
608
|
+
&self,
|
|
609
|
+
key: String,
|
|
610
|
+
value: String,
|
|
611
|
+
kwargs: RHash,
|
|
612
|
+
) -> Result<(), Error> {
|
|
613
|
+
if key.is_empty() {
|
|
614
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let ttl = get_optional::<u64>(&kwargs, "ttl")?;
|
|
618
|
+
let merge_opts = MergeOptions {
|
|
619
|
+
ttl: match ttl {
|
|
620
|
+
Some(ms) => Ttl::ExpireAfter(ms),
|
|
621
|
+
None => Ttl::Default,
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
let write_opts = Self::write_options_from_kwargs(&kwargs)?;
|
|
626
|
+
|
|
627
|
+
block_on_result(async {
|
|
628
|
+
self.inner
|
|
629
|
+
.merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts, &write_opts)
|
|
630
|
+
.await
|
|
631
|
+
})?;
|
|
632
|
+
|
|
633
|
+
Ok(())
|
|
634
|
+
}
|
|
635
|
+
|
|
358
636
|
/// Begin a new transaction.
|
|
359
637
|
///
|
|
360
638
|
/// # Arguments
|
|
@@ -389,12 +667,51 @@ impl Database {
|
|
|
389
667
|
Ok(Snapshot::new(snap))
|
|
390
668
|
}
|
|
391
669
|
|
|
670
|
+
/// Create a checkpoint of the database.
|
|
671
|
+
///
|
|
672
|
+
/// # Arguments
|
|
673
|
+
/// * `kwargs` - Options: lifetime (ms), name
|
|
674
|
+
///
|
|
675
|
+
/// # Returns
|
|
676
|
+
/// Hash with id (UUID string) and manifest_id (int)
|
|
677
|
+
pub fn create_checkpoint(&self, kwargs: RHash) -> Result<RHash, Error> {
|
|
678
|
+
use slatedb::config::{CheckpointOptions, CheckpointScope};
|
|
679
|
+
|
|
680
|
+
let lifetime =
|
|
681
|
+
get_optional::<u64>(&kwargs, "lifetime")?.map(std::time::Duration::from_millis);
|
|
682
|
+
let name = get_optional::<String>(&kwargs, "name")?;
|
|
683
|
+
|
|
684
|
+
let options = CheckpointOptions {
|
|
685
|
+
lifetime,
|
|
686
|
+
source: None,
|
|
687
|
+
name,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
let result = block_on_result(async {
|
|
691
|
+
self.inner
|
|
692
|
+
.create_checkpoint(CheckpointScope::Durable, &options)
|
|
693
|
+
.await
|
|
694
|
+
})?;
|
|
695
|
+
|
|
696
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
697
|
+
let hash = ruby.hash_new();
|
|
698
|
+
hash.aset(ruby.to_symbol("id"), result.id.to_string())?;
|
|
699
|
+
hash.aset(ruby.to_symbol("manifest_id"), result.manifest_id)?;
|
|
700
|
+
|
|
701
|
+
Ok(hash)
|
|
702
|
+
}
|
|
703
|
+
|
|
392
704
|
/// Flush the database to ensure durability.
|
|
393
705
|
pub fn flush(&self) -> Result<(), Error> {
|
|
394
706
|
block_on_result(async { self.inner.flush().await })?;
|
|
395
707
|
Ok(())
|
|
396
708
|
}
|
|
397
709
|
|
|
710
|
+
/// Return the database metrics registry.
|
|
711
|
+
pub fn metrics(&self) -> Result<Metrics, Error> {
|
|
712
|
+
Ok(Metrics::new(self.metrics.clone()))
|
|
713
|
+
}
|
|
714
|
+
|
|
398
715
|
/// Close the database.
|
|
399
716
|
pub fn close(&self) -> Result<(), Error> {
|
|
400
717
|
block_on_result(async { self.inner.close().await })?;
|
|
@@ -407,11 +724,16 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
|
|
|
407
724
|
let class = module.define_class("Database", ruby.class_object())?;
|
|
408
725
|
|
|
409
726
|
// Class methods
|
|
410
|
-
class.define_singleton_method("_open", function!(Database::open,
|
|
727
|
+
class.define_singleton_method("_open", function!(Database::open, 3))?;
|
|
411
728
|
|
|
412
729
|
// Instance methods - simple versions
|
|
413
730
|
class.define_method("_get", method!(Database::get, 1))?;
|
|
414
731
|
class.define_method("_get_with_options", method!(Database::get_with_options, 2))?;
|
|
732
|
+
class.define_method("_get_key_value", method!(Database::get_key_value, 1))?;
|
|
733
|
+
class.define_method(
|
|
734
|
+
"_get_key_value_with_options",
|
|
735
|
+
method!(Database::get_key_value_with_options, 2),
|
|
736
|
+
)?;
|
|
415
737
|
class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
|
|
416
738
|
class.define_method("_put", method!(Database::put, 2))?;
|
|
417
739
|
class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
|
|
@@ -425,17 +747,32 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
|
|
|
425
747
|
"_scan_with_options",
|
|
426
748
|
method!(Database::scan_with_options, 3),
|
|
427
749
|
)?;
|
|
750
|
+
class.define_method("_scan_prefix", method!(Database::scan_prefix, 1))?;
|
|
751
|
+
class.define_method(
|
|
752
|
+
"_scan_prefix_with_options",
|
|
753
|
+
method!(Database::scan_prefix_with_options, 2),
|
|
754
|
+
)?;
|
|
428
755
|
class.define_method("_write", method!(Database::write, 1))?;
|
|
429
756
|
class.define_method(
|
|
430
757
|
"_write_with_options",
|
|
431
758
|
method!(Database::write_with_options, 2),
|
|
432
759
|
)?;
|
|
760
|
+
class.define_method("_merge", method!(Database::merge, 2))?;
|
|
761
|
+
class.define_method(
|
|
762
|
+
"_merge_with_options",
|
|
763
|
+
method!(Database::merge_with_options, 3),
|
|
764
|
+
)?;
|
|
433
765
|
class.define_method(
|
|
434
766
|
"_begin_transaction",
|
|
435
767
|
method!(Database::begin_transaction, 1),
|
|
436
768
|
)?;
|
|
437
769
|
class.define_method("_snapshot", method!(Database::snapshot, 0))?;
|
|
770
|
+
class.define_method(
|
|
771
|
+
"_create_checkpoint",
|
|
772
|
+
method!(Database::create_checkpoint, 1),
|
|
773
|
+
)?;
|
|
438
774
|
class.define_method("flush", method!(Database::flush, 0))?;
|
|
775
|
+
class.define_method("_metrics", method!(Database::metrics, 0))?;
|
|
439
776
|
class.define_method("close", method!(Database::close, 0))?;
|
|
440
777
|
|
|
441
778
|
Ok(())
|
data/ext/slatedb/src/iterator.rs
CHANGED
|
@@ -98,7 +98,10 @@ impl Iterator {
|
|
|
98
98
|
let result = block_on(async {
|
|
99
99
|
let mut guard = inner.lock().await;
|
|
100
100
|
match guard.as_mut() {
|
|
101
|
-
Some(iter) => iter
|
|
101
|
+
Some(iter) => iter
|
|
102
|
+
.seek(key.as_bytes())
|
|
103
|
+
.await
|
|
104
|
+
.map_err(IteratorError::Slate),
|
|
102
105
|
None => Err(IteratorError::Closed),
|
|
103
106
|
}
|
|
104
107
|
});
|
data/ext/slatedb/src/lib.rs
CHANGED
|
@@ -20,6 +20,8 @@ mod admin;
|
|
|
20
20
|
mod database;
|
|
21
21
|
mod errors;
|
|
22
22
|
mod iterator;
|
|
23
|
+
mod merge_ops;
|
|
24
|
+
mod metrics;
|
|
23
25
|
mod reader;
|
|
24
26
|
mod runtime;
|
|
25
27
|
mod snapshot;
|
|
@@ -45,6 +47,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
45
47
|
snapshot::define_snapshot_class(ruby, &module)?;
|
|
46
48
|
reader::define_reader_class(ruby, &module)?;
|
|
47
49
|
admin::define_admin_class(ruby, &module)?;
|
|
50
|
+
metrics::define_metrics_class(ruby, &module)?;
|
|
48
51
|
|
|
49
52
|
Ok(())
|
|
50
53
|
}
|