slatedb 0.2.0 → 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: 0026c9e2a5afd6e0d7fa35c10b037791910a2438cf0bcec73c9935db557c9a85
4
- data.tar.gz: 140bdf13e24d576e87165a63f31e3a0130244f36ea1278da7da5e6703347f7c5
3
+ metadata.gz: 9e7afb38b40ead3216b671173af52bf1a30215979f8e850dbb1e4f7dabf823f3
4
+ data.tar.gz: '03923c7ffdd47ba2ea9d846e98af2f66ce8fc1287e3ed3e99d21e5c27db05a8b'
5
5
  SHA512:
6
- metadata.gz: ae8fe00e7069801bfdf8cf90ea48bfee6a60ed1413cdbc8bef5b9a083744436a0b3d3c1e46ef51c5cbd5de85bd46c6c332ec4b016ffd6482a729fa0b3a50e531
7
- data.tar.gz: 5e60f6509c2eae4c948751d941f6cd7389dae8e43e241d02d559c47251b26383c6a59fcedb50133d54759d9d791a9f07079f6eed37106a386358094f2f3a5b7e
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") }
@@ -196,6 +255,11 @@ db.scan_prefix("order:") do |key, value|
196
255
  puts "#{key}: #{value}"
197
256
  end
198
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
+
199
263
  # Works with transactions, snapshots, and readers too
200
264
  db.transaction do |txn|
201
265
  txn.scan_prefix("item:").each do |k, v|
@@ -429,6 +493,16 @@ SlateDb::Reader.open("/tmp/mydb",
429
493
  checkpoint_id: "uuid-here") do |reader|
430
494
  reader.get("key")
431
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
432
506
  ```
433
507
 
434
508
  ### Admin Operations
@@ -527,7 +601,7 @@ Exception hierarchy:
527
601
 
528
602
  ## Requirements
529
603
 
530
- - Ruby 3.1+
604
+ - Ruby 3.3+
531
605
  - Rust toolchain (for building from source)
532
606
 
533
607
  ## Development
@@ -11,12 +11,13 @@ name = "slatedb"
11
11
  crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
- slatedb = "0.10"
14
+ slatedb = "0.13.1"
15
15
  magnus = { version = "0.8.2", features = ["rb-sys"] }
16
- rb-sys = { version = "0.9.123", features = ["stable-api-compiled-fallback"] }
17
- tokio = { version = "1.47.2", features = ["rt-multi-thread", "sync"] }
18
- bytes = "1.11.0"
19
- url = "2.5.7"
20
- once_cell = "1.21.3"
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"
21
22
  log = "0.4.29"
22
- uuid = "1.19.0"
23
+ uuid = "1.23.1"
@@ -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
  })
@@ -236,6 +249,7 @@ impl Admin {
236
249
  default_opts.compacted_options,
237
250
  ),
238
251
  compactions_options: default_opts.compactions_options,
252
+ detach_options: default_opts.detach_options,
239
253
  }
240
254
  };
241
255
 
@@ -1,4 +1,5 @@
1
- use std::sync::Arc;
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};
@@ -6,11 +7,12 @@ use slatedb::config::{
6
7
  DurabilityLevel, MergeOptions, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions,
7
8
  };
8
9
  use slatedb::object_store::memory::InMemory;
9
- use slatedb::{Db, IsolationLevel};
10
+ use slatedb::{Db, IsolationLevel, IterationOrder, KeyValue};
10
11
 
11
12
  use crate::errors::invalid_argument_error;
12
13
  use crate::iterator::Iterator;
13
14
  use crate::merge_ops::{parse_merge_operator, parse_merge_operator_proc};
15
+ use crate::metrics::Metrics;
14
16
  use crate::runtime::block_on_result;
15
17
  use crate::snapshot::Snapshot;
16
18
  use crate::transaction::Transaction;
@@ -23,9 +25,69 @@ use crate::write_batch::WriteBatch;
23
25
  #[magnus::wrap(class = "SlateDb::Database", free_immediately, size)]
24
26
  pub struct Database {
25
27
  inner: Arc<Db>,
28
+ metrics: Arc<Mutex<HashMap<String, i64>>>,
26
29
  }
27
30
 
28
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
+
29
91
  /// Open a database at the given path.
30
92
  ///
31
93
  /// # Arguments
@@ -37,8 +99,7 @@ impl Database {
37
99
  /// A new Database instance
38
100
  pub fn open(path: String, url: Option<String>, kwargs: RHash) -> Result<Self, Error> {
39
101
  // Try string-based merge operator first, then proc-based
40
- let merge_operator = parse_merge_operator(&kwargs)?
41
- .or(parse_merge_operator_proc(&kwargs)?);
102
+ let merge_operator = parse_merge_operator(&kwargs)?.or(parse_merge_operator_proc(&kwargs)?);
42
103
 
43
104
  let db = block_on_result(async {
44
105
  let object_store: Arc<dyn slatedb::object_store::ObjectStore> =
@@ -58,6 +119,7 @@ impl Database {
58
119
 
59
120
  Ok(Self {
60
121
  inner: Arc::new(db),
122
+ metrics: Arc::new(Mutex::new(HashMap::new())),
61
123
  })
62
124
  }
63
125
 
@@ -77,6 +139,7 @@ impl Database {
77
139
 
78
140
  let result =
79
141
  block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
142
+ self.increment_metric("db.get.count");
80
143
 
81
144
  Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
82
145
  }
@@ -94,31 +157,64 @@ impl Database {
94
157
  return Err(invalid_argument_error("key cannot be empty"));
95
158
  }
96
159
 
97
- let mut opts = ReadOptions::default();
160
+ let opts = Self::read_options_from_kwargs(&kwargs)?;
98
161
 
99
- // Parse durability_filter
100
- if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
101
- opts.durability_filter = match df.as_str() {
102
- "remote" => DurabilityLevel::Remote,
103
- "memory" => DurabilityLevel::Memory,
104
- other => {
105
- return Err(invalid_argument_error(&format!(
106
- "invalid durability_filter: {} (expected 'remote' or 'memory')",
107
- other
108
- )))
109
- }
110
- };
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"));
111
179
  }
112
180
 
113
- // Parse dirty
114
- if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
115
- opts.dirty = dirty;
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"));
116
207
  }
117
208
 
118
- let result =
119
- block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
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");
120
216
 
121
- Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
217
+ result.map(Self::key_value_to_hash).transpose()
122
218
  }
123
219
 
124
220
  /// Get a value by key as raw bytes.
@@ -137,6 +233,7 @@ impl Database {
137
233
 
138
234
  let result =
139
235
  block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
236
+ self.increment_metric("db.get_bytes.count");
140
237
 
141
238
  Ok(result.map(|b| b.to_vec()))
142
239
  }
@@ -155,6 +252,7 @@ impl Database {
155
252
 
156
253
  let write_opts = WriteOptions {
157
254
  await_durable: true,
255
+ seqnum: 0,
158
256
  };
159
257
 
160
258
  block_on_result(async {
@@ -162,6 +260,7 @@ impl Database {
162
260
  .put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
163
261
  .await
164
262
  })?;
263
+ self.increment_metric("db.put.count");
165
264
 
166
265
  Ok(())
167
266
  }
@@ -171,7 +270,7 @@ impl Database {
171
270
  /// # Arguments
172
271
  /// * `key` - The key to store
173
272
  /// * `value` - The value to store
174
- /// * `kwargs` - Keyword arguments (ttl, await_durable)
273
+ /// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
175
274
  pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
176
275
  if key.is_empty() {
177
276
  return Err(invalid_argument_error("key cannot be empty"));
@@ -187,14 +286,14 @@ impl Database {
187
286
  };
188
287
 
189
288
  // Parse await_durable
190
- let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
191
- let write_opts = WriteOptions { await_durable };
289
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
192
290
 
193
291
  block_on_result(async {
194
292
  self.inner
195
293
  .put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
196
294
  .await
197
295
  })?;
296
+ self.increment_metric("db.put_with_options.count");
198
297
 
199
298
  Ok(())
200
299
  }
@@ -210,6 +309,7 @@ impl Database {
210
309
 
211
310
  let write_opts = WriteOptions {
212
311
  await_durable: true,
312
+ seqnum: 0,
213
313
  };
214
314
 
215
315
  block_on_result(async {
@@ -217,6 +317,7 @@ impl Database {
217
317
  .delete_with_options(key.as_bytes(), &write_opts)
218
318
  .await
219
319
  })?;
320
+ self.increment_metric("db.delete.count");
220
321
 
221
322
  Ok(())
222
323
  }
@@ -225,20 +326,20 @@ impl Database {
225
326
  ///
226
327
  /// # Arguments
227
328
  /// * `key` - The key to delete
228
- /// * `kwargs` - Keyword arguments (await_durable)
329
+ /// * `kwargs` - Keyword arguments (await_durable, seqnum)
229
330
  pub fn delete_with_options(&self, key: String, kwargs: RHash) -> Result<(), Error> {
230
331
  if key.is_empty() {
231
332
  return Err(invalid_argument_error("key cannot be empty"));
232
333
  }
233
334
 
234
- let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
235
- let write_opts = WriteOptions { await_durable };
335
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
236
336
 
237
337
  block_on_result(async {
238
338
  self.inner
239
339
  .delete_with_options(key.as_bytes(), &write_opts)
240
340
  .await
241
341
  })?;
342
+ self.increment_metric("db.delete_with_options.count");
242
343
 
243
344
  Ok(())
244
345
  }
@@ -325,6 +426,18 @@ impl Database {
325
426
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
326
427
  opts.max_fetch_tasks = mft;
327
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
+ }
328
441
 
329
442
  let start_bytes = start.into_bytes();
330
443
  let end_bytes = end_key.map(|e| e.into_bytes());
@@ -408,6 +521,18 @@ impl Database {
408
521
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
409
522
  opts.max_fetch_tasks = mft;
410
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
+ }
411
536
 
412
537
  let iter = block_on_result(async {
413
538
  self.inner
@@ -432,10 +557,9 @@ impl Database {
432
557
  ///
433
558
  /// # Arguments
434
559
  /// * `batch` - The WriteBatch to write
435
- /// * `kwargs` - Keyword arguments (await_durable)
560
+ /// * `kwargs` - Keyword arguments (await_durable, seqnum)
436
561
  pub fn write_with_options(&self, batch: &WriteBatch, kwargs: RHash) -> Result<(), Error> {
437
- let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
438
- let write_opts = WriteOptions { await_durable };
562
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
439
563
 
440
564
  let batch_inner = batch.take()?;
441
565
 
@@ -462,6 +586,7 @@ impl Database {
462
586
 
463
587
  let write_opts = WriteOptions {
464
588
  await_durable: true,
589
+ seqnum: 0,
465
590
  };
466
591
 
467
592
  block_on_result(async {
@@ -478,8 +603,13 @@ impl Database {
478
603
  /// # Arguments
479
604
  /// * `key` - The key to merge into
480
605
  /// * `value` - The merge operand to apply
481
- /// * `kwargs` - Keyword arguments (ttl, await_durable)
482
- pub fn merge_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
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> {
483
613
  if key.is_empty() {
484
614
  return Err(invalid_argument_error("key cannot be empty"));
485
615
  }
@@ -492,8 +622,7 @@ impl Database {
492
622
  },
493
623
  };
494
624
 
495
- let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
496
- let write_opts = WriteOptions { await_durable };
625
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
497
626
 
498
627
  block_on_result(async {
499
628
  self.inner
@@ -548,8 +677,8 @@ impl Database {
548
677
  pub fn create_checkpoint(&self, kwargs: RHash) -> Result<RHash, Error> {
549
678
  use slatedb::config::{CheckpointOptions, CheckpointScope};
550
679
 
551
- let lifetime = get_optional::<u64>(&kwargs, "lifetime")?
552
- .map(std::time::Duration::from_millis);
680
+ let lifetime =
681
+ get_optional::<u64>(&kwargs, "lifetime")?.map(std::time::Duration::from_millis);
553
682
  let name = get_optional::<String>(&kwargs, "name")?;
554
683
 
555
684
  let options = CheckpointOptions {
@@ -578,6 +707,11 @@ impl Database {
578
707
  Ok(())
579
708
  }
580
709
 
710
+ /// Return the database metrics registry.
711
+ pub fn metrics(&self) -> Result<Metrics, Error> {
712
+ Ok(Metrics::new(self.metrics.clone()))
713
+ }
714
+
581
715
  /// Close the database.
582
716
  pub fn close(&self) -> Result<(), Error> {
583
717
  block_on_result(async { self.inner.close().await })?;
@@ -595,6 +729,11 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
595
729
  // Instance methods - simple versions
596
730
  class.define_method("_get", method!(Database::get, 1))?;
597
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
+ )?;
598
737
  class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
599
738
  class.define_method("_put", method!(Database::put, 2))?;
600
739
  class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
@@ -633,6 +772,7 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
633
772
  method!(Database::create_checkpoint, 1),
634
773
  )?;
635
774
  class.define_method("flush", method!(Database::flush, 0))?;
775
+ class.define_method("_metrics", method!(Database::metrics, 0))?;
636
776
  class.define_method("close", method!(Database::close, 0))?;
637
777
 
638
778
  Ok(())
@@ -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.seek(key.as_bytes()).await.map_err(IteratorError::Slate),
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
  });
@@ -21,6 +21,7 @@ mod database;
21
21
  mod errors;
22
22
  mod iterator;
23
23
  mod merge_ops;
24
+ mod metrics;
24
25
  mod reader;
25
26
  mod runtime;
26
27
  mod snapshot;
@@ -46,6 +47,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
46
47
  snapshot::define_snapshot_class(ruby, &module)?;
47
48
  reader::define_reader_class(ruby, &module)?;
48
49
  admin::define_admin_class(ruby, &module)?;
50
+ metrics::define_metrics_class(ruby, &module)?;
49
51
 
50
52
  Ok(())
51
53
  }
@@ -149,9 +149,7 @@ impl RubyProcMergeOperator {
149
149
  "Ruby merge operator called from non-Ruby thread, using fallback concatenation. \
150
150
  This can happen during background compaction."
151
151
  );
152
- let mut result = existing_value
153
- .map(|v| v.to_vec())
154
- .unwrap_or_default();
152
+ let mut result = existing_value.map(|v| v.to_vec()).unwrap_or_default();
155
153
  result.extend_from_slice(new_value);
156
154
  Ok(Bytes::from(result))
157
155
  }