slatedb 0.2.0 → 0.4.4

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,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,15 +7,16 @@ 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;
17
- use crate::utils::{get_optional, resolve_object_store};
19
+ use crate::utils::{get_optional, prefix_subrange_from_kwargs, resolve_object_store};
18
20
  use crate::write_batch::WriteBatch;
19
21
 
20
22
  /// Ruby wrapper for SlateDB database.
@@ -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());
@@ -354,7 +467,7 @@ impl Database {
354
467
  let opts = ScanOptions::default();
355
468
  let iter = block_on_result(async {
356
469
  self.inner
357
- .scan_prefix_with_options(prefix.as_bytes(), &opts)
470
+ .scan_prefix_with_options(prefix.as_bytes(), .., &opts)
358
471
  .await
359
472
  })?;
360
473
 
@@ -408,10 +521,23 @@ 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
 
537
+ let subrange = prefix_subrange_from_kwargs(&kwargs)?;
412
538
  let iter = block_on_result(async {
413
539
  self.inner
414
- .scan_prefix_with_options(prefix.as_bytes(), &opts)
540
+ .scan_prefix_with_options(prefix.as_bytes(), subrange, &opts)
415
541
  .await
416
542
  })?;
417
543
 
@@ -432,10 +558,9 @@ impl Database {
432
558
  ///
433
559
  /// # Arguments
434
560
  /// * `batch` - The WriteBatch to write
435
- /// * `kwargs` - Keyword arguments (await_durable)
561
+ /// * `kwargs` - Keyword arguments (await_durable, seqnum)
436
562
  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 };
563
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
439
564
 
440
565
  let batch_inner = batch.take()?;
441
566
 
@@ -462,6 +587,7 @@ impl Database {
462
587
 
463
588
  let write_opts = WriteOptions {
464
589
  await_durable: true,
590
+ seqnum: 0,
465
591
  };
466
592
 
467
593
  block_on_result(async {
@@ -478,8 +604,13 @@ impl Database {
478
604
  /// # Arguments
479
605
  /// * `key` - The key to merge into
480
606
  /// * `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> {
607
+ /// * `kwargs` - Keyword arguments (ttl, await_durable, seqnum)
608
+ pub fn merge_with_options(
609
+ &self,
610
+ key: String,
611
+ value: String,
612
+ kwargs: RHash,
613
+ ) -> Result<(), Error> {
483
614
  if key.is_empty() {
484
615
  return Err(invalid_argument_error("key cannot be empty"));
485
616
  }
@@ -492,8 +623,7 @@ impl Database {
492
623
  },
493
624
  };
494
625
 
495
- let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
496
- let write_opts = WriteOptions { await_durable };
626
+ let write_opts = Self::write_options_from_kwargs(&kwargs)?;
497
627
 
498
628
  block_on_result(async {
499
629
  self.inner
@@ -548,8 +678,8 @@ impl Database {
548
678
  pub fn create_checkpoint(&self, kwargs: RHash) -> Result<RHash, Error> {
549
679
  use slatedb::config::{CheckpointOptions, CheckpointScope};
550
680
 
551
- let lifetime = get_optional::<u64>(&kwargs, "lifetime")?
552
- .map(std::time::Duration::from_millis);
681
+ let lifetime =
682
+ get_optional::<u64>(&kwargs, "lifetime")?.map(std::time::Duration::from_millis);
553
683
  let name = get_optional::<String>(&kwargs, "name")?;
554
684
 
555
685
  let options = CheckpointOptions {
@@ -578,6 +708,11 @@ impl Database {
578
708
  Ok(())
579
709
  }
580
710
 
711
+ /// Return the database metrics registry.
712
+ pub fn metrics(&self) -> Result<Metrics, Error> {
713
+ Ok(Metrics::new(self.metrics.clone()))
714
+ }
715
+
581
716
  /// Close the database.
582
717
  pub fn close(&self) -> Result<(), Error> {
583
718
  block_on_result(async { self.inner.close().await })?;
@@ -595,6 +730,11 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
595
730
  // Instance methods - simple versions
596
731
  class.define_method("_get", method!(Database::get, 1))?;
597
732
  class.define_method("_get_with_options", method!(Database::get_with_options, 2))?;
733
+ class.define_method("_get_key_value", method!(Database::get_key_value, 1))?;
734
+ class.define_method(
735
+ "_get_key_value_with_options",
736
+ method!(Database::get_key_value_with_options, 2),
737
+ )?;
598
738
  class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
599
739
  class.define_method("_put", method!(Database::put, 2))?;
600
740
  class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
@@ -633,6 +773,7 @@ pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<()
633
773
  method!(Database::create_checkpoint, 1),
634
774
  )?;
635
775
  class.define_method("flush", method!(Database::flush, 0))?;
776
+ class.define_method("_metrics", method!(Database::metrics, 0))?;
636
777
  class.define_method("close", method!(Database::close, 0))?;
637
778
 
638
779
  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
  }
@@ -0,0 +1,47 @@
1
+ use std::collections::HashMap;
2
+ use std::sync::{Arc, Mutex};
3
+
4
+ use magnus::prelude::*;
5
+ use magnus::{method, Error, Ruby};
6
+ /// Ruby wrapper for SlateDB metrics registry.
7
+ ///
8
+ /// This struct is exposed to Ruby as `SlateDb::Metrics`.
9
+ #[magnus::wrap(class = "SlateDb::Metrics", free_immediately, size)]
10
+ pub struct Metrics {
11
+ inner: Arc<Mutex<HashMap<String, i64>>>,
12
+ }
13
+
14
+ impl Metrics {
15
+ pub fn new(inner: Arc<Mutex<HashMap<String, i64>>>) -> Self {
16
+ Self { inner }
17
+ }
18
+
19
+ /// Return a list of metric names.
20
+ pub fn names(&self) -> Result<magnus::RArray, Error> {
21
+ let metrics = self.inner.lock().expect("metrics mutex poisoned");
22
+ let ruby = Ruby::get().expect("Ruby runtime not available");
23
+ let result = ruby.ary_new_capa(metrics.len());
24
+
25
+ for name in metrics.keys() {
26
+ result.push(ruby.str_new(name))?;
27
+ }
28
+
29
+ Ok(result)
30
+ }
31
+
32
+ /// Get the current value of a metric by name.
33
+ pub fn get(&self, name: String) -> Result<Option<i64>, Error> {
34
+ let metrics = self.inner.lock().expect("metrics mutex poisoned");
35
+ Ok(metrics.get(&name).copied())
36
+ }
37
+ }
38
+
39
+ /// Define the Metrics class on the SlateDb module.
40
+ pub fn define_metrics_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
41
+ let class = module.define_class("Metrics", ruby.class_object())?;
42
+
43
+ class.define_method("names", method!(Metrics::names, 0))?;
44
+ class.define_method("get", method!(Metrics::get, 1))?;
45
+
46
+ Ok(())
47
+ }