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.
@@ -1,13 +1,18 @@
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};
5
- use slatedb::config::{DurabilityLevel, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions};
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>) -> Result<Self, Error> {
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> = if let Some(ref url_str) = url {
37
- resolve_object_store(url_str)?
38
- } else {
39
- Arc::new(InMemory::new())
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
- Db::builder(path, object_store).build().await
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 mut opts = ReadOptions::default();
160
+ let opts = Self::read_options_from_kwargs(&kwargs)?;
84
161
 
85
- // Parse durability_filter
86
- if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
87
- opts.durability_filter = match df.as_str() {
88
- "remote" => DurabilityLevel::Remote,
89
- "memory" => DurabilityLevel::Memory,
90
- other => {
91
- return Err(invalid_argument_error(&format!(
92
- "invalid durability_filter: {} (expected 'remote' or 'memory')",
93
- other
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
- // Parse dirty
100
- if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
101
- 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"));
102
207
  }
103
208
 
104
- let result =
105
- 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");
106
216
 
107
- Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
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 await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
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 await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
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 await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
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, 2))?;
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(())
@@ -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
  });
@@ -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
  }