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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c93234ef6d9251d54b90cd816983c1d073d159587ae655fbef8e4e3b433928a3
4
- data.tar.gz: 0b7aa7b9137ff2228f7e4153da2daafbb235ce857b51faa8d552c55f18cb7e7a
3
+ metadata.gz: 9e7afb38b40ead3216b671173af52bf1a30215979f8e850dbb1e4f7dabf823f3
4
+ data.tar.gz: '03923c7ffdd47ba2ea9d846e98af2f66ce8fc1287e3ed3e99d21e5c27db05a8b'
5
5
  SHA512:
6
- metadata.gz: 54cc3a48576fc4ca07dc6ae95a755aa6fc5dd7fa7af46eba3452f3465c74d005d9f29d06ac1cc9ddf3461d602d910d63a08f55b0d486140ed22024f3ce1e3e4e
7
- data.tar.gz: 2bd1c37ca9fcfa737bd47fd3d76b136376819b573a08e044c06114f5555d5243e8b465b7bbc8cba02f3461982cc5ea1e4195fb31e53a5f0d4dcd608d1ea79f55
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.1+
604
+ - Ruby 3.3+
390
605
  - Rust toolchain (for building from source)
391
606
 
392
607
  ## Development
@@ -11,13 +11,13 @@ name = "slatedb"
11
11
  crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
- slatedb = "0.9"
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
- object_store = { version = "0.12", features = ["aws"] }
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"
@@ -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