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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e7afb38b40ead3216b671173af52bf1a30215979f8e850dbb1e4f7dabf823f3
|
|
4
|
+
data.tar.gz: '03923c7ffdd47ba2ea9d846e98af2f66ce8fc1287e3ed3e99d21e5c27db05a8b'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d693ec11b74c8eea9722a270f48ccf8d209f21138a24dac54fbfa5d8242e23233982ea1f94aa31a667d4c0902c524213d20a9872fb6fb538685446d0efbb4fd1
|
|
7
|
+
data.tar.gz: 2e1b75894800650de82f8671bd07eefc678b6cfaaf6259245fbf854f8faaa0b8651f5c0ba1f07f4be5a8b0d85b2b54ca19177d2801a1f0f91400dde42202477a
|
data/README.md
CHANGED
|
@@ -138,6 +138,34 @@ db.put("key", "value", ttl: 60_000) # expires in 60 seconds
|
|
|
138
138
|
|
|
139
139
|
# Don't wait for durability
|
|
140
140
|
db.put("key", "value", await_durable: false)
|
|
141
|
+
|
|
142
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
143
|
+
db.put("key", "value", seqnum: 42)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### User-Supplied Sequence Numbers
|
|
147
|
+
|
|
148
|
+
By default SlateDB assigns a monotonically increasing sequence number to every
|
|
149
|
+
write. Since SlateDB 0.13.0 you can instead supply your own via `seqnum:`. The
|
|
150
|
+
value must be **strictly greater** than the current maximum sequence number, or
|
|
151
|
+
the write is rejected with `SlateDb::InvalidArgumentError`. This is useful when
|
|
152
|
+
replaying an external log or coordinating sequence numbers across systems.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
db.put("key", "value", seqnum: 1_000)
|
|
156
|
+
db.delete("old", seqnum: 1_001)
|
|
157
|
+
db.merge("counter", "5", seqnum: 1_002) # requires a merge operator
|
|
158
|
+
db.write(batch, seqnum: 1_003) # applied across the batch
|
|
159
|
+
db.batch(seqnum: 1_004) { |b| b.put("k", "v") }
|
|
160
|
+
|
|
161
|
+
# The sequence number is reflected in the stored record
|
|
162
|
+
db.put("key", "value", seqnum: 2_000)
|
|
163
|
+
db.get_key_value("key")[:seq] # => 2000
|
|
164
|
+
|
|
165
|
+
# On a transaction it is supplied at commit time
|
|
166
|
+
txn = db.begin_transaction
|
|
167
|
+
txn.put("k", "v")
|
|
168
|
+
txn.commit(seqnum: 3_000)
|
|
141
169
|
```
|
|
142
170
|
|
|
143
171
|
#### Get Options
|
|
@@ -151,11 +179,37 @@ db.get("key", durability_filter: "remote")
|
|
|
151
179
|
db.get("key", dirty: true)
|
|
152
180
|
```
|
|
153
181
|
|
|
182
|
+
#### Key-Value Metadata
|
|
183
|
+
|
|
184
|
+
SlateDB can return the full key-value record, including storage metadata:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
db.put("key", "value")
|
|
188
|
+
entry = db.get_key_value("key")
|
|
189
|
+
# => { key: "key", value: "value", seq: 1, create_ts: 1_765_000_000_000, expire_ts: nil }
|
|
190
|
+
|
|
191
|
+
entry[:value] # => "value"
|
|
192
|
+
entry[:seq] # SlateDB sequence number
|
|
193
|
+
entry[:create_ts] # creation timestamp in milliseconds
|
|
194
|
+
entry[:expire_ts] # expiration timestamp in milliseconds, or nil
|
|
195
|
+
|
|
196
|
+
# Alias for the same API
|
|
197
|
+
db.get_entry("key")
|
|
198
|
+
|
|
199
|
+
# The same read options accepted by #get are supported
|
|
200
|
+
db.get_key_value("key", durability_filter: "memory", cache_blocks: false)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Missing keys return `nil`, matching `#get`.
|
|
204
|
+
|
|
154
205
|
#### Delete Options
|
|
155
206
|
|
|
156
207
|
```ruby
|
|
157
208
|
# Don't wait for durability
|
|
158
209
|
db.delete("key", await_durable: false)
|
|
210
|
+
|
|
211
|
+
# Supply an explicit sequence number (SlateDB >= 0.13.0)
|
|
212
|
+
db.delete("key", seqnum: 42)
|
|
159
213
|
```
|
|
160
214
|
|
|
161
215
|
### Scanning
|
|
@@ -173,6 +227,11 @@ db.scan("a", "z").each do |key, value|
|
|
|
173
227
|
puts "#{key}: #{value}"
|
|
174
228
|
end
|
|
175
229
|
|
|
230
|
+
# Scan in descending key order
|
|
231
|
+
db.scan("a", "z", order: :desc).each do |key, value|
|
|
232
|
+
puts "#{key}: #{value}"
|
|
233
|
+
end
|
|
234
|
+
|
|
176
235
|
# Use Enumerable methods
|
|
177
236
|
keys = db.scan("user:").map { |k, v| k }
|
|
178
237
|
users = db.scan("user:").select { |k, v| v.include?("active") }
|
|
@@ -181,6 +240,110 @@ users = db.scan("user:").select { |k, v| v.include?("active") }
|
|
|
181
240
|
all_entries = db.scan("").to_a
|
|
182
241
|
```
|
|
183
242
|
|
|
243
|
+
#### Prefix Scanning
|
|
244
|
+
|
|
245
|
+
Scan all keys with a given prefix using `scan_prefix`:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# Scan all keys starting with "user:"
|
|
249
|
+
db.scan_prefix("user:").each do |key, value|
|
|
250
|
+
puts "#{key}: #{value}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Block form
|
|
254
|
+
db.scan_prefix("order:") do |key, value|
|
|
255
|
+
puts "#{key}: #{value}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Prefix scans can also run in descending key order
|
|
259
|
+
db.scan_prefix("user:", order: :desc).each do |key, value|
|
|
260
|
+
puts "#{key}: #{value}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Works with transactions, snapshots, and readers too
|
|
264
|
+
db.transaction do |txn|
|
|
265
|
+
txn.scan_prefix("item:").each do |k, v|
|
|
266
|
+
puts "#{k}: #{v}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Merge Operations
|
|
272
|
+
|
|
273
|
+
Merge operations allow you to combine values without reading them first, useful for counters, append-only logs, and similar patterns:
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# Open with a built-in merge operator
|
|
277
|
+
SlateDb::Database.open("/tmp/mydb", merge_operator: :string_concat) do |db|
|
|
278
|
+
# Merge appends to existing values (or creates if key doesn't exist)
|
|
279
|
+
db.merge("log", "line1\n")
|
|
280
|
+
db.merge("log", "line2\n")
|
|
281
|
+
db.merge("log", "line3\n")
|
|
282
|
+
|
|
283
|
+
db.get("log") # => "line1\nline2\nline3\n"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Merge with options
|
|
287
|
+
db.merge("key", "value", ttl: 60_000, await_durable: false)
|
|
288
|
+
|
|
289
|
+
# Works in transactions and batches
|
|
290
|
+
db.transaction do |txn|
|
|
291
|
+
txn.merge("counter", "1")
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
db.batch do |b|
|
|
295
|
+
b.merge("key", "a")
|
|
296
|
+
.merge("key", "b")
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Custom Merge Operators
|
|
301
|
+
|
|
302
|
+
You can provide a Ruby Proc/lambda as a custom merge operator:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
# Counter merge operator (adds numbers)
|
|
306
|
+
counter_merge = ->(key, existing, new_value) {
|
|
307
|
+
existing_num = existing ? existing.to_i : 0
|
|
308
|
+
(existing_num + new_value.to_i).to_s
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
SlateDb::Database.open("/tmp/mydb", merge_operator: counter_merge) do |db|
|
|
312
|
+
db.merge("visits", "1")
|
|
313
|
+
db.merge("visits", "1")
|
|
314
|
+
db.merge("visits", "1")
|
|
315
|
+
|
|
316
|
+
db.get("visits") # => "3"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Max value merge operator
|
|
320
|
+
max_merge = ->(key, existing, new_value) {
|
|
321
|
+
existing_num = existing ? existing.to_i : 0
|
|
322
|
+
new_num = new_value.to_i
|
|
323
|
+
[existing_num, new_num].max.to_s
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
SlateDb::Database.open("/tmp/mydb", merge_operator: max_merge) do |db|
|
|
327
|
+
db.merge("high_score", "100")
|
|
328
|
+
db.merge("high_score", "250")
|
|
329
|
+
db.merge("high_score", "150")
|
|
330
|
+
|
|
331
|
+
db.get("high_score") # => "250"
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The proc receives three arguments:
|
|
336
|
+
- `key` - The key being merged
|
|
337
|
+
- `existing` - The existing value (nil if no value exists)
|
|
338
|
+
- `new_value` - The new merge operand
|
|
339
|
+
|
|
340
|
+
**Note:** Custom Proc merge operators work best with direct `db.merge()` calls. When used with transactions or batches, some merge operations may be processed on background threads and fall back to string concatenation.
|
|
341
|
+
|
|
342
|
+
#### Available Merge Operators
|
|
343
|
+
|
|
344
|
+
- `:string_concat` (or `:concat`) - Concatenates byte values (built-in)
|
|
345
|
+
- Any `Proc` or `lambda` - Custom merge logic
|
|
346
|
+
|
|
184
347
|
### Write Batches
|
|
185
348
|
|
|
186
349
|
Perform multiple writes atomically:
|
|
@@ -231,18 +394,60 @@ Transaction operations:
|
|
|
231
394
|
db.transaction do |txn|
|
|
232
395
|
# Read
|
|
233
396
|
value = txn.get("key")
|
|
234
|
-
|
|
397
|
+
|
|
235
398
|
# Write
|
|
236
399
|
txn.put("key", "value")
|
|
237
400
|
txn.put("expiring", "data", ttl: 30_000)
|
|
238
|
-
|
|
401
|
+
|
|
239
402
|
# Delete
|
|
240
403
|
txn.delete("old_key")
|
|
241
|
-
|
|
404
|
+
|
|
242
405
|
# Scan
|
|
243
406
|
txn.scan("prefix:").each do |k, v|
|
|
244
407
|
puts "#{k}: #{v}"
|
|
245
408
|
end
|
|
409
|
+
|
|
410
|
+
# Scan with prefix
|
|
411
|
+
txn.scan_prefix("user:").each do |k, v|
|
|
412
|
+
puts "#{k}: #{v}"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Explicit Read Tracking
|
|
418
|
+
|
|
419
|
+
In serializable transactions, use `mark_read` to explicitly track keys for conflict detection without actually reading them:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
db.transaction(isolation: :serializable) do |txn|
|
|
423
|
+
# Mark keys as read for conflict detection
|
|
424
|
+
txn.mark_read(["key1", "key2", "key3"])
|
|
425
|
+
|
|
426
|
+
# Now if another transaction modifies key1/key2/key3,
|
|
427
|
+
# this transaction will fail on commit
|
|
428
|
+
txn.put("result", "computed_value")
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Checkpoints
|
|
433
|
+
|
|
434
|
+
Create durable checkpoints for backup or read replica purposes:
|
|
435
|
+
|
|
436
|
+
```ruby
|
|
437
|
+
SlateDb::Database.open("/tmp/mydb", url: "file:///tmp/mydb") do |db|
|
|
438
|
+
db.put("key", "value")
|
|
439
|
+
db.flush
|
|
440
|
+
|
|
441
|
+
# Create a checkpoint
|
|
442
|
+
checkpoint = db.create_checkpoint
|
|
443
|
+
puts "Checkpoint ID: #{checkpoint[:id]}"
|
|
444
|
+
puts "Manifest ID: #{checkpoint[:manifest_id]}"
|
|
445
|
+
|
|
446
|
+
# Create a named checkpoint with lifetime
|
|
447
|
+
checkpoint = db.create_checkpoint(
|
|
448
|
+
name: "before-migration",
|
|
449
|
+
lifetime: 3_600_000 # 1 hour in milliseconds
|
|
450
|
+
)
|
|
246
451
|
end
|
|
247
452
|
```
|
|
248
453
|
|
|
@@ -288,6 +493,16 @@ SlateDb::Reader.open("/tmp/mydb",
|
|
|
288
493
|
checkpoint_id: "uuid-here") do |reader|
|
|
289
494
|
reader.get("key")
|
|
290
495
|
end
|
|
496
|
+
|
|
497
|
+
# Enable the reader's on-disk cache and cap its open file handles
|
|
498
|
+
# (max_open_file_handles, added in SlateDB 0.13.0, only takes effect when
|
|
499
|
+
# cache_root is set, since that is what enables the cached object store).
|
|
500
|
+
SlateDb::Reader.open("/tmp/mydb",
|
|
501
|
+
url: "s3://bucket/path",
|
|
502
|
+
cache_root: "/var/cache/slatedb",
|
|
503
|
+
max_open_file_handles: 256) do |reader|
|
|
504
|
+
reader.get("key")
|
|
505
|
+
end
|
|
291
506
|
```
|
|
292
507
|
|
|
293
508
|
### Admin Operations
|
|
@@ -386,7 +601,7 @@ Exception hierarchy:
|
|
|
386
601
|
|
|
387
602
|
## Requirements
|
|
388
603
|
|
|
389
|
-
- Ruby 3.
|
|
604
|
+
- Ruby 3.3+
|
|
390
605
|
- Rust toolchain (for building from source)
|
|
391
606
|
|
|
392
607
|
## Development
|
data/ext/slatedb/Cargo.toml
CHANGED
|
@@ -11,13 +11,13 @@ name = "slatedb"
|
|
|
11
11
|
crate-type = ["cdylib"]
|
|
12
12
|
|
|
13
13
|
[dependencies]
|
|
14
|
-
slatedb = "0.
|
|
15
|
-
magnus = { version = "0.8", features = ["rb-sys"] }
|
|
16
|
-
rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
|
|
17
|
-
tokio = { version = "1", features = ["rt-multi-thread", "sync"] }
|
|
18
|
-
bytes = "1"
|
|
19
|
-
|
|
20
|
-
url = "2"
|
|
21
|
-
once_cell = "1"
|
|
22
|
-
log = "0.4"
|
|
23
|
-
uuid = "1"
|
|
14
|
+
slatedb = "0.13.1"
|
|
15
|
+
magnus = { version = "0.8.2", features = ["rb-sys"] }
|
|
16
|
+
rb-sys = { version = "0.9.128", features = ["stable-api-compiled-fallback"] }
|
|
17
|
+
tokio = { version = "1.52.3", features = ["rt-multi-thread", "sync"] }
|
|
18
|
+
bytes = "1.11.1"
|
|
19
|
+
serde_json = "1.0.145"
|
|
20
|
+
url = "2.5.8"
|
|
21
|
+
once_cell = "1.21.4"
|
|
22
|
+
log = "0.4.29"
|
|
23
|
+
uuid = "1.23.1"
|
data/ext/slatedb/src/admin.rs
CHANGED
|
@@ -25,10 +25,10 @@ impl Admin {
|
|
|
25
25
|
/// * `path` - The path identifier for the database
|
|
26
26
|
/// * `url` - Optional object store URL
|
|
27
27
|
pub fn new(path: String, url: Option<String>) -> Result<Self, Error> {
|
|
28
|
-
let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) = url {
|
|
28
|
+
let object_store: Arc<dyn slatedb::object_store::ObjectStore> = if let Some(ref url) = url {
|
|
29
29
|
block_on_result(async { resolve_object_store(url) })?
|
|
30
30
|
} else {
|
|
31
|
-
Arc::new(object_store::memory::InMemory::new())
|
|
31
|
+
Arc::new(slatedb::object_store::memory::InMemory::new())
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
let admin = AdminBuilder::new(path, object_store).build();
|
|
@@ -43,10 +43,18 @@ impl Admin {
|
|
|
43
43
|
/// # Returns
|
|
44
44
|
/// JSON string of the manifest, or None if no manifests exist.
|
|
45
45
|
pub fn read_manifest(&self, id: Option<u64>) -> Result<Option<String>, Error> {
|
|
46
|
-
block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
|
|
46
|
+
let manifest = block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
|
|
47
47
|
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
48
48
|
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
49
|
-
})
|
|
49
|
+
})?;
|
|
50
|
+
|
|
51
|
+
match manifest {
|
|
52
|
+
Some(manifest) => Ok(Some(serde_json::to_string(&manifest).map_err(|e| {
|
|
53
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
54
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
55
|
+
})?)),
|
|
56
|
+
None => Ok(None),
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/// List manifests within an optional [start, end) range as JSON.
|
|
@@ -65,7 +73,12 @@ impl Admin {
|
|
|
65
73
|
(None, None) => 0..u64::MAX,
|
|
66
74
|
};
|
|
67
75
|
|
|
68
|
-
block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
|
|
76
|
+
let manifests = block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
|
|
77
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
78
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
79
|
+
})?;
|
|
80
|
+
|
|
81
|
+
serde_json::to_string(&manifests).map_err(|e| {
|
|
69
82
|
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
70
83
|
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
71
84
|
})
|
|
@@ -235,6 +248,8 @@ impl Admin {
|
|
|
235
248
|
min_age,
|
|
236
249
|
default_opts.compacted_options,
|
|
237
250
|
),
|
|
251
|
+
compactions_options: default_opts.compactions_options,
|
|
252
|
+
detach_options: default_opts.detach_options,
|
|
238
253
|
}
|
|
239
254
|
};
|
|
240
255
|
|