slatedb 0.1.1 → 0.2.0
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 +144 -3
- data/ext/slatedb/Cargo.toml +9 -10
- data/ext/slatedb/src/admin.rs +3 -2
- data/ext/slatedb/src/database.rs +206 -9
- data/ext/slatedb/src/lib.rs +1 -0
- data/ext/slatedb/src/merge_ops.rs +242 -0
- data/ext/slatedb/src/reader.rs +74 -3
- data/ext/slatedb/src/runtime.rs +52 -1
- data/ext/slatedb/src/snapshot.rs +76 -0
- data/ext/slatedb/src/transaction.rs +147 -5
- data/ext/slatedb/src/utils.rs +5 -4
- data/ext/slatedb/src/write_batch.rs +43 -1
- data/lib/slatedb/database.rb +109 -3
- data/lib/slatedb/reader.rb +35 -1
- data/lib/slatedb/snapshot.rb +32 -0
- data/lib/slatedb/transaction.rb +67 -0
- data/lib/slatedb/version.rb +1 -1
- data/lib/slatedb/write_batch.rb +20 -0
- metadata +3 -2
|
@@ -2,8 +2,10 @@ 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;
|
|
7
9
|
|
|
8
10
|
use crate::errors::{closed_error, invalid_argument_error, map_error};
|
|
9
11
|
use crate::iterator::Iterator;
|
|
@@ -16,12 +18,12 @@ use crate::utils::get_optional;
|
|
|
16
18
|
/// After commit or rollback, the transaction is closed.
|
|
17
19
|
#[magnus::wrap(class = "SlateDb::Transaction", free_immediately, size)]
|
|
18
20
|
pub struct Transaction {
|
|
19
|
-
inner: RefCell<Option<
|
|
21
|
+
inner: RefCell<Option<DbTransaction>>,
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
impl Transaction {
|
|
23
|
-
/// Create a new Transaction from a
|
|
24
|
-
pub fn new(txn:
|
|
25
|
+
/// Create a new Transaction from a DbTransaction.
|
|
26
|
+
pub fn new(txn: DbTransaction) -> Self {
|
|
25
27
|
Self {
|
|
26
28
|
inner: RefCell::new(Some(txn)),
|
|
27
29
|
}
|
|
@@ -135,6 +137,48 @@ impl Transaction {
|
|
|
135
137
|
Ok(())
|
|
136
138
|
}
|
|
137
139
|
|
|
140
|
+
/// Merge a value within the transaction.
|
|
141
|
+
pub fn merge(&self, key: String, value: String) -> Result<(), Error> {
|
|
142
|
+
if key.is_empty() {
|
|
143
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let guard = self.inner.borrow();
|
|
147
|
+
let txn = guard
|
|
148
|
+
.as_ref()
|
|
149
|
+
.ok_or_else(|| closed_error("transaction is closed"))?;
|
|
150
|
+
|
|
151
|
+
txn.merge(key.as_bytes(), value.as_bytes())
|
|
152
|
+
.map_err(map_error)?;
|
|
153
|
+
|
|
154
|
+
Ok(())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Merge a value with options within the transaction.
|
|
158
|
+
pub fn merge_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
|
|
159
|
+
if key.is_empty() {
|
|
160
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let ttl = get_optional::<u64>(&kwargs, "ttl")?;
|
|
164
|
+
let merge_opts = MergeOptions {
|
|
165
|
+
ttl: match ttl {
|
|
166
|
+
Some(ms) => Ttl::ExpireAfter(ms),
|
|
167
|
+
None => Ttl::Default,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
let guard = self.inner.borrow();
|
|
172
|
+
let txn = guard
|
|
173
|
+
.as_ref()
|
|
174
|
+
.ok_or_else(|| closed_error("transaction is closed"))?;
|
|
175
|
+
|
|
176
|
+
txn.merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts)
|
|
177
|
+
.map_err(map_error)?;
|
|
178
|
+
|
|
179
|
+
Ok(())
|
|
180
|
+
}
|
|
181
|
+
|
|
138
182
|
/// Scan a range of keys within the transaction.
|
|
139
183
|
pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
|
|
140
184
|
if start.is_empty() {
|
|
@@ -219,6 +263,93 @@ impl Transaction {
|
|
|
219
263
|
Ok(Iterator::new(iter))
|
|
220
264
|
}
|
|
221
265
|
|
|
266
|
+
/// Scan all keys with a given prefix within the transaction.
|
|
267
|
+
pub fn scan_prefix(&self, prefix: String) -> Result<Iterator, Error> {
|
|
268
|
+
if prefix.is_empty() {
|
|
269
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let guard = self.inner.borrow();
|
|
273
|
+
let txn = guard
|
|
274
|
+
.as_ref()
|
|
275
|
+
.ok_or_else(|| closed_error("transaction is closed"))?;
|
|
276
|
+
|
|
277
|
+
let iter = block_on_result(async { txn.scan_prefix(prefix.as_bytes()).await })?;
|
|
278
|
+
|
|
279
|
+
Ok(Iterator::new(iter))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Scan all keys with a given prefix with options within the transaction.
|
|
283
|
+
pub fn scan_prefix_with_options(
|
|
284
|
+
&self,
|
|
285
|
+
prefix: String,
|
|
286
|
+
kwargs: RHash,
|
|
287
|
+
) -> Result<Iterator, Error> {
|
|
288
|
+
if prefix.is_empty() {
|
|
289
|
+
return Err(invalid_argument_error("prefix cannot be empty"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let mut opts = ScanOptions::default();
|
|
293
|
+
|
|
294
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
295
|
+
opts.durability_filter = match df.as_str() {
|
|
296
|
+
"remote" => DurabilityLevel::Remote,
|
|
297
|
+
"memory" => DurabilityLevel::Memory,
|
|
298
|
+
other => {
|
|
299
|
+
return Err(invalid_argument_error(&format!(
|
|
300
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
301
|
+
other
|
|
302
|
+
)))
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
308
|
+
opts.dirty = dirty;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
|
|
312
|
+
opts.read_ahead_bytes = rab;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
316
|
+
opts.cache_blocks = cb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
320
|
+
opts.max_fetch_tasks = mft;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let guard = self.inner.borrow();
|
|
324
|
+
let txn = guard
|
|
325
|
+
.as_ref()
|
|
326
|
+
.ok_or_else(|| closed_error("transaction is closed"))?;
|
|
327
|
+
|
|
328
|
+
let iter =
|
|
329
|
+
block_on_result(async { txn.scan_prefix_with_options(prefix.as_bytes(), &opts).await })?;
|
|
330
|
+
|
|
331
|
+
Ok(Iterator::new(iter))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Mark keys as read for conflict detection.
|
|
335
|
+
///
|
|
336
|
+
/// This explicitly tracks reads for conflict checking in serializable isolation,
|
|
337
|
+
/// even when the keys weren't actually read via get().
|
|
338
|
+
///
|
|
339
|
+
/// # Arguments
|
|
340
|
+
/// * `keys` - Array of keys to mark as read
|
|
341
|
+
pub fn mark_read(&self, keys: Vec<String>) -> Result<(), Error> {
|
|
342
|
+
let guard = self.inner.borrow();
|
|
343
|
+
let txn = guard
|
|
344
|
+
.as_ref()
|
|
345
|
+
.ok_or_else(|| closed_error("transaction is closed"))?;
|
|
346
|
+
|
|
347
|
+
let key_bytes: Vec<&[u8]> = keys.iter().map(|k| k.as_bytes()).collect();
|
|
348
|
+
txn.mark_read(&key_bytes).map_err(map_error)?;
|
|
349
|
+
|
|
350
|
+
Ok(())
|
|
351
|
+
}
|
|
352
|
+
|
|
222
353
|
/// Commit the transaction.
|
|
223
354
|
pub fn commit(&self) -> Result<(), Error> {
|
|
224
355
|
let txn = self
|
|
@@ -275,11 +406,22 @@ pub fn define_transaction_class(ruby: &Ruby, module: &magnus::RModule) -> Result
|
|
|
275
406
|
method!(Transaction::put_with_options, 3),
|
|
276
407
|
)?;
|
|
277
408
|
class.define_method("_delete", method!(Transaction::delete, 1))?;
|
|
409
|
+
class.define_method("_merge", method!(Transaction::merge, 2))?;
|
|
410
|
+
class.define_method(
|
|
411
|
+
"_merge_with_options",
|
|
412
|
+
method!(Transaction::merge_with_options, 3),
|
|
413
|
+
)?;
|
|
278
414
|
class.define_method("_scan", method!(Transaction::scan, 2))?;
|
|
279
415
|
class.define_method(
|
|
280
416
|
"_scan_with_options",
|
|
281
417
|
method!(Transaction::scan_with_options, 3),
|
|
282
418
|
)?;
|
|
419
|
+
class.define_method("_scan_prefix", method!(Transaction::scan_prefix, 1))?;
|
|
420
|
+
class.define_method(
|
|
421
|
+
"_scan_prefix_with_options",
|
|
422
|
+
method!(Transaction::scan_prefix_with_options, 2),
|
|
423
|
+
)?;
|
|
424
|
+
class.define_method("_mark_read", method!(Transaction::mark_read, 1))?;
|
|
283
425
|
class.define_method("commit", method!(Transaction::commit, 0))?;
|
|
284
426
|
class.define_method(
|
|
285
427
|
"_commit_with_options",
|
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,43 @@ 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(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
|
|
93
|
+
if key.is_empty() {
|
|
94
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let ttl = get_optional::<u64>(&kwargs, "ttl")?;
|
|
98
|
+
let merge_opts = MergeOptions {
|
|
99
|
+
ttl: match ttl {
|
|
100
|
+
Some(ms) => Ttl::ExpireAfter(ms),
|
|
101
|
+
None => Ttl::Default,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
self.inner
|
|
106
|
+
.borrow_mut()
|
|
107
|
+
.merge_with_options(key.as_bytes(), value.as_bytes(), &merge_opts);
|
|
108
|
+
|
|
109
|
+
Ok(())
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
/// Take ownership of the inner WriteBatch (consumes it).
|
|
76
113
|
/// Used internally when writing the batch to the database.
|
|
77
114
|
pub fn take(&self) -> Result<SlateWriteBatch, Error> {
|
|
@@ -93,6 +130,11 @@ pub fn define_write_batch_class(ruby: &Ruby, module: &magnus::RModule) -> Result
|
|
|
93
130
|
method!(WriteBatch::put_with_options, 3),
|
|
94
131
|
)?;
|
|
95
132
|
class.define_method("_delete", method!(WriteBatch::delete, 1))?;
|
|
133
|
+
class.define_method("_merge", method!(WriteBatch::merge, 2))?;
|
|
134
|
+
class.define_method(
|
|
135
|
+
"_merge_with_options",
|
|
136
|
+
method!(WriteBatch::merge_with_options, 3),
|
|
137
|
+
)?;
|
|
96
138
|
|
|
97
139
|
Ok(())
|
|
98
140
|
}
|
data/lib/slatedb/database.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SlateDb
|
|
4
|
-
class Database
|
|
4
|
+
class Database # rubocop:disable Metrics/ClassLength
|
|
5
5
|
private_class_method :new
|
|
6
6
|
|
|
7
7
|
class << self
|
|
@@ -9,6 +9,9 @@ module SlateDb
|
|
|
9
9
|
#
|
|
10
10
|
# @param path [String] The path identifier for the database
|
|
11
11
|
# @param url [String, nil] Optional object store URL (e.g., "s3://bucket/path")
|
|
12
|
+
# @param merge_operator [Symbol, String, Proc, nil] Optional merge operator.
|
|
13
|
+
# Can be a symbol/string ("string_concat" or "concat") or a Proc/lambda
|
|
14
|
+
# that takes (key, existing_value, new_value) and returns the merged value.
|
|
12
15
|
# @yield [db] If a block is given, yields the database and ensures it's closed
|
|
13
16
|
# @return [Database] The opened database (or block result if block given)
|
|
14
17
|
#
|
|
@@ -25,8 +28,29 @@ module SlateDb
|
|
|
25
28
|
# @example Open with S3 backend
|
|
26
29
|
# db = SlateDb::Database.open("/tmp/mydb", url: "s3://mybucket/path")
|
|
27
30
|
#
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
# @example Open with a custom merge operator (Proc)
|
|
32
|
+
# # Custom merge that adds numbers
|
|
33
|
+
# db = SlateDb::Database.open("/tmp/mydb", merge_operator: ->(key, existing, new_val) {
|
|
34
|
+
# existing_num = existing ? existing.to_i : 0
|
|
35
|
+
# (existing_num + new_val.to_i).to_s
|
|
36
|
+
# })
|
|
37
|
+
# db.merge("counter", "5")
|
|
38
|
+
# db.merge("counter", "3")
|
|
39
|
+
# db.get("counter") # => "8"
|
|
40
|
+
#
|
|
41
|
+
def open(path, url: nil, merge_operator: nil)
|
|
42
|
+
opts = {}
|
|
43
|
+
|
|
44
|
+
case merge_operator
|
|
45
|
+
when Symbol, String
|
|
46
|
+
opts[:merge_operator] = merge_operator.to_s
|
|
47
|
+
when Proc
|
|
48
|
+
# Store the proc to prevent GC and pass to Rust
|
|
49
|
+
@_merge_operator_proc = merge_operator
|
|
50
|
+
opts[:merge_operator_proc] = merge_operator
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
db = _open(path, url, opts)
|
|
30
54
|
|
|
31
55
|
if block_given?
|
|
32
56
|
begin
|
|
@@ -168,6 +192,43 @@ module SlateDb
|
|
|
168
192
|
end
|
|
169
193
|
end
|
|
170
194
|
|
|
195
|
+
# Scan all keys with a given prefix.
|
|
196
|
+
#
|
|
197
|
+
# @param prefix [String] The key prefix to scan
|
|
198
|
+
# @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
|
|
199
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
200
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
201
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
202
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
203
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
204
|
+
#
|
|
205
|
+
# @example Scan all user keys
|
|
206
|
+
# db.scan_prefix("user:") do |key, value|
|
|
207
|
+
# puts "#{key}: #{value}"
|
|
208
|
+
# end
|
|
209
|
+
#
|
|
210
|
+
def scan_prefix(prefix, durability_filter: nil, dirty: nil,
|
|
211
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
|
|
212
|
+
opts = {}
|
|
213
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
214
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
215
|
+
opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
|
|
216
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
217
|
+
opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
|
|
218
|
+
|
|
219
|
+
iter = if opts.empty?
|
|
220
|
+
_scan_prefix(prefix)
|
|
221
|
+
else
|
|
222
|
+
_scan_prefix_with_options(prefix, opts)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if block_given?
|
|
226
|
+
iter.each(&)
|
|
227
|
+
else
|
|
228
|
+
iter
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
171
232
|
# Write a batch of operations atomically.
|
|
172
233
|
#
|
|
173
234
|
# @param batch [WriteBatch] The batch to write
|
|
@@ -195,6 +256,31 @@ module SlateDb
|
|
|
195
256
|
end
|
|
196
257
|
end
|
|
197
258
|
|
|
259
|
+
# Merge a value into the database.
|
|
260
|
+
#
|
|
261
|
+
# @param key [String] The key to merge into
|
|
262
|
+
# @param value [String] The merge operand to apply
|
|
263
|
+
# @param ttl [Integer, nil] Time-to-live in milliseconds
|
|
264
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
265
|
+
# @return [void]
|
|
266
|
+
#
|
|
267
|
+
# @example Merge with string concatenation operator
|
|
268
|
+
# db = SlateDb::Database.open("/tmp/mydb", merge_operator: :string_concat)
|
|
269
|
+
# db.merge("key", "part1")
|
|
270
|
+
# db.merge("key", "part2")
|
|
271
|
+
#
|
|
272
|
+
def merge(key, value, ttl: nil, await_durable: nil)
|
|
273
|
+
opts = {}
|
|
274
|
+
opts[:ttl] = ttl if ttl
|
|
275
|
+
opts[:await_durable] = await_durable unless await_durable.nil?
|
|
276
|
+
|
|
277
|
+
if opts.empty?
|
|
278
|
+
_merge(key, value)
|
|
279
|
+
else
|
|
280
|
+
_merge_with_options(key, value, opts)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
198
284
|
# Create and write a batch using a block.
|
|
199
285
|
#
|
|
200
286
|
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
@@ -308,5 +394,25 @@ module SlateDb
|
|
|
308
394
|
snap
|
|
309
395
|
end
|
|
310
396
|
end
|
|
397
|
+
|
|
398
|
+
# Create a checkpoint of the database.
|
|
399
|
+
#
|
|
400
|
+
# @param lifetime [Integer, nil] Checkpoint lifetime in milliseconds
|
|
401
|
+
# @param name [String, nil] Optional name for the checkpoint
|
|
402
|
+
# @return [Hash] Hash with :id (UUID string) and :manifest_id (integer)
|
|
403
|
+
#
|
|
404
|
+
# @example Create a named checkpoint
|
|
405
|
+
# checkpoint = db.create_checkpoint(name: "before-migration")
|
|
406
|
+
# puts "Checkpoint ID: #{checkpoint[:id]}"
|
|
407
|
+
#
|
|
408
|
+
# @example Create a checkpoint with lifetime
|
|
409
|
+
# checkpoint = db.create_checkpoint(lifetime: 3600_000) # 1 hour
|
|
410
|
+
#
|
|
411
|
+
def create_checkpoint(lifetime: nil, name: nil)
|
|
412
|
+
opts = {}
|
|
413
|
+
opts[:lifetime] = lifetime if lifetime
|
|
414
|
+
opts[:name] = name if name
|
|
415
|
+
_create_checkpoint(opts)
|
|
416
|
+
end
|
|
311
417
|
end
|
|
312
418
|
end
|
data/lib/slatedb/reader.rb
CHANGED
|
@@ -11,6 +11,7 @@ module SlateDb
|
|
|
11
11
|
# @param manifest_poll_interval [Integer, nil] Poll interval in milliseconds
|
|
12
12
|
# @param checkpoint_lifetime [Integer, nil] Checkpoint lifetime in milliseconds
|
|
13
13
|
# @param max_memtable_bytes [Integer, nil] Maximum memtable size in bytes
|
|
14
|
+
# @param merge_operator [Symbol, String, nil] Optional merge operator ("string_concat" or "concat")
|
|
14
15
|
# @yield [reader] If a block is given, yields the reader and ensures it's closed
|
|
15
16
|
# @return [Reader] The opened reader (or block result if block given)
|
|
16
17
|
#
|
|
@@ -29,11 +30,12 @@ module SlateDb
|
|
|
29
30
|
#
|
|
30
31
|
def open(path, url: nil, checkpoint_id: nil,
|
|
31
32
|
manifest_poll_interval: nil, checkpoint_lifetime: nil,
|
|
32
|
-
max_memtable_bytes: nil)
|
|
33
|
+
max_memtable_bytes: nil, merge_operator: nil)
|
|
33
34
|
opts = {}
|
|
34
35
|
opts[:manifest_poll_interval] = manifest_poll_interval if manifest_poll_interval
|
|
35
36
|
opts[:checkpoint_lifetime] = checkpoint_lifetime if checkpoint_lifetime
|
|
36
37
|
opts[:max_memtable_bytes] = max_memtable_bytes if max_memtable_bytes
|
|
38
|
+
opts[:merge_operator] = merge_operator.to_s if merge_operator
|
|
37
39
|
|
|
38
40
|
reader = _open(path, url, checkpoint_id, opts)
|
|
39
41
|
|
|
@@ -101,5 +103,37 @@ module SlateDb
|
|
|
101
103
|
iter
|
|
102
104
|
end
|
|
103
105
|
end
|
|
106
|
+
|
|
107
|
+
# Scan all keys with a given prefix.
|
|
108
|
+
#
|
|
109
|
+
# @param prefix [String] The key prefix to scan
|
|
110
|
+
# @param durability_filter [String, nil] Filter by durability level
|
|
111
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
112
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
113
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
114
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
115
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
116
|
+
#
|
|
117
|
+
def scan_prefix(prefix, durability_filter: nil, dirty: nil,
|
|
118
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
|
|
119
|
+
opts = {}
|
|
120
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
121
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
122
|
+
opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
|
|
123
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
124
|
+
opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
|
|
125
|
+
|
|
126
|
+
iter = if opts.empty?
|
|
127
|
+
_scan_prefix(prefix)
|
|
128
|
+
else
|
|
129
|
+
_scan_prefix_with_options(prefix, opts)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if block_given?
|
|
133
|
+
iter.each(&)
|
|
134
|
+
else
|
|
135
|
+
iter
|
|
136
|
+
end
|
|
137
|
+
end
|
|
104
138
|
end
|
|
105
139
|
end
|
data/lib/slatedb/snapshot.rb
CHANGED
|
@@ -50,5 +50,37 @@ module SlateDb
|
|
|
50
50
|
iter
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Scan all keys with a given prefix from the snapshot.
|
|
55
|
+
#
|
|
56
|
+
# @param prefix [String] The key prefix to scan
|
|
57
|
+
# @param durability_filter [String, nil] Filter by durability level
|
|
58
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
59
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
60
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
61
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
62
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
63
|
+
#
|
|
64
|
+
def scan_prefix(prefix, durability_filter: nil, dirty: nil,
|
|
65
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
|
|
66
|
+
opts = {}
|
|
67
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
68
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
69
|
+
opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
|
|
70
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
71
|
+
opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
|
|
72
|
+
|
|
73
|
+
iter = if opts.empty?
|
|
74
|
+
_scan_prefix(prefix)
|
|
75
|
+
else
|
|
76
|
+
_scan_prefix_with_options(prefix, opts)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if block_given?
|
|
80
|
+
iter.each(&)
|
|
81
|
+
else
|
|
82
|
+
iter
|
|
83
|
+
end
|
|
84
|
+
end
|
|
53
85
|
end
|
|
54
86
|
end
|
data/lib/slatedb/transaction.rb
CHANGED
|
@@ -47,6 +47,21 @@ module SlateDb
|
|
|
47
47
|
_delete(key)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Merge a value within the transaction.
|
|
51
|
+
#
|
|
52
|
+
# @param key [String] The key to merge into
|
|
53
|
+
# @param value [String] The merge operand to apply
|
|
54
|
+
# @param ttl [Integer, nil] Time-to-live in milliseconds
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
def merge(key, value, ttl: nil)
|
|
58
|
+
if ttl
|
|
59
|
+
_merge_with_options(key, value, { ttl: ttl })
|
|
60
|
+
else
|
|
61
|
+
_merge(key, value)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
50
65
|
# Scan a range of keys within the transaction.
|
|
51
66
|
#
|
|
52
67
|
# @param start_key [String] The start key (inclusive)
|
|
@@ -74,5 +89,57 @@ module SlateDb
|
|
|
74
89
|
iter
|
|
75
90
|
end
|
|
76
91
|
end
|
|
92
|
+
|
|
93
|
+
# Scan all keys with a given prefix within the transaction.
|
|
94
|
+
#
|
|
95
|
+
# @param prefix [String] The key prefix to scan
|
|
96
|
+
# @param durability_filter [String, nil] Filter by durability level
|
|
97
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
98
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
99
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
100
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
101
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
102
|
+
#
|
|
103
|
+
def scan_prefix(prefix, durability_filter: nil, dirty: nil,
|
|
104
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
|
|
105
|
+
opts = {}
|
|
106
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
107
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
108
|
+
opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
|
|
109
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
110
|
+
opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
|
|
111
|
+
|
|
112
|
+
iter = if opts.empty?
|
|
113
|
+
_scan_prefix(prefix)
|
|
114
|
+
else
|
|
115
|
+
_scan_prefix_with_options(prefix, opts)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if block_given?
|
|
119
|
+
iter.each(&)
|
|
120
|
+
else
|
|
121
|
+
iter
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Mark keys as read for conflict detection.
|
|
126
|
+
#
|
|
127
|
+
# This explicitly tracks reads for conflict checking in serializable isolation,
|
|
128
|
+
# allowing selective read-write conflict detection even when keys weren't
|
|
129
|
+
# actually read via get().
|
|
130
|
+
#
|
|
131
|
+
# @param keys [Array<String>] The keys to mark as read
|
|
132
|
+
# @return [void]
|
|
133
|
+
#
|
|
134
|
+
# @example Mark keys for conflict detection
|
|
135
|
+
# db.transaction(isolation: :serializable) do |txn|
|
|
136
|
+
# txn.mark_read(["key1", "key2"])
|
|
137
|
+
# # These keys will now be checked for conflicts on commit
|
|
138
|
+
# txn.put("key3", "value")
|
|
139
|
+
# end
|
|
140
|
+
#
|
|
141
|
+
def mark_read(keys)
|
|
142
|
+
_mark_read(Array(keys))
|
|
143
|
+
end
|
|
77
144
|
end
|
|
78
145
|
end
|
data/lib/slatedb/version.rb
CHANGED
data/lib/slatedb/write_batch.rb
CHANGED
|
@@ -34,5 +34,25 @@ module SlateDb
|
|
|
34
34
|
_delete(key)
|
|
35
35
|
self
|
|
36
36
|
end
|
|
37
|
+
|
|
38
|
+
# Add a merge operation to the batch.
|
|
39
|
+
#
|
|
40
|
+
# @param key [String] The key to merge into
|
|
41
|
+
# @param value [String] The merge operand to apply
|
|
42
|
+
# @param ttl [Integer, nil] Time-to-live in milliseconds
|
|
43
|
+
# @return [self] Returns self for method chaining
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# batch.merge("key", "part1")
|
|
47
|
+
# batch.merge("key", "part2", ttl: 30_000)
|
|
48
|
+
#
|
|
49
|
+
def merge(key, value, ttl: nil)
|
|
50
|
+
if ttl
|
|
51
|
+
_merge_with_options(key, value, { ttl: ttl })
|
|
52
|
+
else
|
|
53
|
+
_merge(key, value)
|
|
54
|
+
end
|
|
55
|
+
self
|
|
56
|
+
end
|
|
37
57
|
end
|
|
38
58
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slatedb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SlateDB Contributors
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- ext/slatedb/src/errors.rs
|
|
98
98
|
- ext/slatedb/src/iterator.rs
|
|
99
99
|
- ext/slatedb/src/lib.rs
|
|
100
|
+
- ext/slatedb/src/merge_ops.rs
|
|
100
101
|
- ext/slatedb/src/reader.rs
|
|
101
102
|
- ext/slatedb/src/runtime.rs
|
|
102
103
|
- ext/slatedb/src/snapshot.rs
|
|
@@ -134,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
135
|
- !ruby/object:Gem::Version
|
|
135
136
|
version: '0'
|
|
136
137
|
requirements: []
|
|
137
|
-
rubygems_version:
|
|
138
|
+
rubygems_version: 4.0.2
|
|
138
139
|
specification_version: 4
|
|
139
140
|
summary: Ruby bindings for SlateDB
|
|
140
141
|
test_files: []
|