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.
@@ -4,12 +4,13 @@ use magnus::prelude::*;
4
4
  use magnus::{function, method, Error, RHash, Ruby};
5
5
  use slatedb::config::{DbReaderOptions, DurabilityLevel, ReadOptions, ScanOptions};
6
6
  use slatedb::DbReader;
7
+ use slatedb::DbReaderMode;
8
+ use slatedb::IterationOrder;
7
9
 
8
10
  use crate::errors::invalid_argument_error;
9
11
  use crate::iterator::Iterator;
10
- use crate::merge_ops::parse_merge_operator;
11
12
  use crate::runtime::block_on_result;
12
- use crate::utils::{get_optional, resolve_object_store};
13
+ use crate::utils::{get_optional, prefix_subrange_from_kwargs, resolve_object_store};
13
14
 
14
15
  /// Ruby wrapper for SlateDB Reader.
15
16
  ///
@@ -26,8 +27,20 @@ impl Reader {
26
27
  /// # Arguments
27
28
  /// * `path` - The path identifier for the database
28
29
  /// * `url` - Optional object store URL
29
- /// * `checkpoint_id` - Optional checkpoint UUID to read at
30
- /// * `kwargs` - Additional options (manifest_poll_interval, checkpoint_lifetime, max_memtable_bytes, merge_operator)
30
+ /// * `checkpoint_id` - Optional checkpoint UUID to read at. When set, the reader
31
+ /// is pinned to that checkpoint ([`DbReaderMode::Checkpoint`]).
32
+ /// * `kwargs` - Additional options (manifest_poll_interval, checkpoint_lifetime,
33
+ /// max_memtable_bytes, skip_wal_replay, cache_root, max_open_file_handles,
34
+ /// follow_latest).
35
+ /// The local disk cache (and therefore `max_open_file_handles`) is only active
36
+ /// when `cache_root` is set.
37
+ ///
38
+ /// When neither `checkpoint_id` nor `follow_latest` is given the reader defaults
39
+ /// to [`DbReaderMode::ManagedCheckpoint`], creating and periodically refreshing
40
+ /// its own checkpoint so garbage collection cannot delete objects out from under
41
+ /// it. Setting `follow_latest: true` selects [`DbReaderMode::FollowLatest`], which
42
+ /// tails the latest manifest without writing any checkpoint — useful for read-only
43
+ /// or mirrored databases, at the cost of no protection from garbage collection.
31
44
  pub fn open(
32
45
  path: String,
33
46
  url: Option<String>,
@@ -40,24 +53,35 @@ impl Reader {
40
53
  let checkpoint_lifetime = get_optional::<u64>(&kwargs, "checkpoint_lifetime")?
41
54
  .map(std::time::Duration::from_millis);
42
55
  let max_memtable_bytes = get_optional::<u64>(&kwargs, "max_memtable_bytes")?;
43
- let merge_operator = parse_merge_operator(&kwargs)?;
44
-
45
- // Parse checkpoint_id as UUID
46
- let checkpoint_uuid =
47
- if let Some(id_str) = checkpoint_id {
48
- Some(uuid::Uuid::parse_str(&id_str).map_err(|e| {
56
+ let skip_wal_replay = get_optional::<bool>(&kwargs, "skip_wal_replay")?;
57
+ let max_open_file_handles = get_optional::<usize>(&kwargs, "max_open_file_handles")?;
58
+ let cache_root = get_optional::<String>(&kwargs, "cache_root")?;
59
+ let follow_latest = get_optional::<bool>(&kwargs, "follow_latest")?.unwrap_or(false);
60
+
61
+ // Resolve the reader mode from checkpoint_id / follow_latest.
62
+ let mode = match checkpoint_id {
63
+ Some(id_str) => {
64
+ if follow_latest {
65
+ return Err(invalid_argument_error(
66
+ "checkpoint_id and follow_latest are mutually exclusive",
67
+ ));
68
+ }
69
+ let uuid = uuid::Uuid::parse_str(&id_str).map_err(|e| {
49
70
  invalid_argument_error(&format!("invalid checkpoint_id: {}", e))
50
- })?)
51
- } else {
52
- None
53
- };
71
+ })?;
72
+ DbReaderMode::Checkpoint(uuid)
73
+ }
74
+ None if follow_latest => DbReaderMode::FollowLatest,
75
+ None => DbReaderMode::ManagedCheckpoint,
76
+ };
54
77
 
55
78
  let reader = block_on_result(async {
56
- let object_store: Arc<dyn slatedb::object_store::ObjectStore> = if let Some(ref url) = url {
57
- resolve_object_store(url)?
58
- } else {
59
- Arc::new(slatedb::object_store::memory::InMemory::new())
60
- };
79
+ let object_store: Arc<dyn slatedb::object_store::ObjectStore> =
80
+ if let Some(ref url) = url {
81
+ resolve_object_store(url)?
82
+ } else {
83
+ Arc::new(slatedb::object_store::memory::InMemory::new())
84
+ };
61
85
 
62
86
  let mut options = DbReaderOptions::default();
63
87
  if let Some(interval) = manifest_poll_interval {
@@ -69,11 +93,17 @@ impl Reader {
69
93
  if let Some(max_bytes) = max_memtable_bytes {
70
94
  options.max_memtable_bytes = max_bytes;
71
95
  }
72
- if let Some(merge_operator) = merge_operator {
73
- options.merge_operator = Some(merge_operator);
96
+ if let Some(skip_replay) = skip_wal_replay {
97
+ options.skip_wal_replay = skip_replay;
74
98
  }
75
-
76
- DbReader::open(path, object_store, checkpoint_uuid, options).await
99
+ if let Some(ref root) = cache_root {
100
+ options.object_store_cache_options.root_folder =
101
+ Some(std::path::PathBuf::from(root));
102
+ }
103
+ if let Some(max_handles) = max_open_file_handles {
104
+ options.object_store_cache_options.max_open_file_handles = max_handles;
105
+ }
106
+ DbReader::open(path, object_store, mode, options).await
77
107
  })?;
78
108
 
79
109
  Ok(Self {
@@ -116,6 +146,10 @@ impl Reader {
116
146
  opts.dirty = dirty;
117
147
  }
118
148
 
149
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
150
+ opts.cache_blocks = cb;
151
+ }
152
+
119
153
  let result =
120
154
  block_on_result(async { self.inner.get_with_options(key.as_bytes(), &opts).await })?;
121
155
  Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
@@ -191,6 +225,18 @@ impl Reader {
191
225
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
192
226
  opts.max_fetch_tasks = mft;
193
227
  }
228
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
229
+ opts.order = match order.as_str() {
230
+ "ascending" | "asc" => IterationOrder::Ascending,
231
+ "descending" | "desc" => IterationOrder::Descending,
232
+ other => {
233
+ return Err(invalid_argument_error(&format!(
234
+ "invalid order: {} (expected 'asc' or 'desc')",
235
+ other
236
+ )))
237
+ }
238
+ };
239
+ }
194
240
 
195
241
  let start_bytes = start.into_bytes();
196
242
  let end_bytes = end_key.map(|e| e.into_bytes());
@@ -211,7 +257,8 @@ impl Reader {
211
257
  return Err(invalid_argument_error("prefix cannot be empty"));
212
258
  }
213
259
 
214
- let iter = block_on_result(async { self.inner.scan_prefix(prefix.as_bytes()).await })?;
260
+ let iter =
261
+ block_on_result(async { self.inner.scan_prefix(prefix.as_bytes(), ..).await })?;
215
262
 
216
263
  Ok(Iterator::new(iter))
217
264
  }
@@ -256,10 +303,23 @@ impl Reader {
256
303
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
257
304
  opts.max_fetch_tasks = mft;
258
305
  }
306
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
307
+ opts.order = match order.as_str() {
308
+ "ascending" | "asc" => IterationOrder::Ascending,
309
+ "descending" | "desc" => IterationOrder::Descending,
310
+ other => {
311
+ return Err(invalid_argument_error(&format!(
312
+ "invalid order: {} (expected 'asc' or 'desc')",
313
+ other
314
+ )))
315
+ }
316
+ };
317
+ }
259
318
 
319
+ let subrange = prefix_subrange_from_kwargs(&kwargs)?;
260
320
  let iter = block_on_result(async {
261
321
  self.inner
262
- .scan_prefix_with_options(prefix.as_bytes(), &opts)
322
+ .scan_prefix_with_options(prefix.as_bytes(), subrange, &opts)
263
323
  .await
264
324
  })?;
265
325
 
@@ -5,11 +5,12 @@ use magnus::prelude::*;
5
5
  use magnus::{method, Error, RHash, Ruby};
6
6
  use slatedb::config::{DurabilityLevel, ReadOptions, ScanOptions};
7
7
  use slatedb::DbSnapshot;
8
+ use slatedb::IterationOrder;
8
9
 
9
10
  use crate::errors::{closed_error, invalid_argument_error};
10
11
  use crate::iterator::Iterator;
11
12
  use crate::runtime::block_on_result;
12
- use crate::utils::get_optional;
13
+ use crate::utils::{get_optional, prefix_subrange_from_kwargs};
13
14
 
14
15
  /// Ruby wrapper for SlateDB Snapshot.
15
16
  ///
@@ -68,6 +69,10 @@ impl Snapshot {
68
69
  opts.dirty = dirty;
69
70
  }
70
71
 
72
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
73
+ opts.cache_blocks = cb;
74
+ }
75
+
71
76
  let guard = self.inner.borrow();
72
77
  let snapshot = guard
73
78
  .as_ref()
@@ -143,6 +148,18 @@ impl Snapshot {
143
148
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
144
149
  opts.max_fetch_tasks = mft;
145
150
  }
151
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
152
+ opts.order = match order.as_str() {
153
+ "ascending" | "asc" => IterationOrder::Ascending,
154
+ "descending" | "desc" => IterationOrder::Descending,
155
+ other => {
156
+ return Err(invalid_argument_error(&format!(
157
+ "invalid order: {} (expected 'asc' or 'desc')",
158
+ other
159
+ )))
160
+ }
161
+ };
162
+ }
146
163
 
147
164
  let guard = self.inner.borrow();
148
165
  let snapshot = guard
@@ -173,7 +190,7 @@ impl Snapshot {
173
190
  .as_ref()
174
191
  .ok_or_else(|| closed_error("snapshot is closed"))?;
175
192
 
176
- let iter = block_on_result(async { snapshot.scan_prefix(prefix.as_bytes()).await })?;
193
+ let iter = block_on_result(async { snapshot.scan_prefix(prefix.as_bytes(), ..).await })?;
177
194
 
178
195
  Ok(Iterator::new(iter))
179
196
  }
@@ -218,6 +235,20 @@ impl Snapshot {
218
235
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
219
236
  opts.max_fetch_tasks = mft;
220
237
  }
238
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
239
+ opts.order = match order.as_str() {
240
+ "ascending" | "asc" => IterationOrder::Ascending,
241
+ "descending" | "desc" => IterationOrder::Descending,
242
+ other => {
243
+ return Err(invalid_argument_error(&format!(
244
+ "invalid order: {} (expected 'asc' or 'desc')",
245
+ other
246
+ )))
247
+ }
248
+ };
249
+ }
250
+
251
+ let subrange = prefix_subrange_from_kwargs(&kwargs)?;
221
252
 
222
253
  let guard = self.inner.borrow();
223
254
  let snapshot = guard
@@ -226,7 +257,7 @@ impl Snapshot {
226
257
 
227
258
  let iter = block_on_result(async {
228
259
  snapshot
229
- .scan_prefix_with_options(prefix.as_bytes(), &opts)
260
+ .scan_prefix_with_options(prefix.as_bytes(), subrange, &opts)
230
261
  .await
231
262
  })?;
232
263
 
@@ -6,11 +6,12 @@ use slatedb::config::{
6
6
  DurabilityLevel, MergeOptions, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions,
7
7
  };
8
8
  use slatedb::DbTransaction;
9
+ use slatedb::IterationOrder;
9
10
 
10
11
  use crate::errors::{closed_error, invalid_argument_error, map_error};
11
12
  use crate::iterator::Iterator;
12
13
  use crate::runtime::block_on_result;
13
- use crate::utils::get_optional;
14
+ use crate::utils::{get_optional, prefix_subrange_from_kwargs};
14
15
 
15
16
  /// Ruby wrapper for SlateDB Transaction.
16
17
  ///
@@ -69,13 +70,16 @@ impl Transaction {
69
70
  opts.dirty = dirty;
70
71
  }
71
72
 
73
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
74
+ opts.cache_blocks = cb;
75
+ }
76
+
72
77
  let guard = self.inner.borrow();
73
78
  let txn = guard
74
79
  .as_ref()
75
80
  .ok_or_else(|| closed_error("transaction is closed"))?;
76
81
 
77
- let result =
78
- block_on_result(async { txn.get_with_options(key.as_bytes(), &opts).await })?;
82
+ let result = block_on_result(async { txn.get_with_options(key.as_bytes(), &opts).await })?;
79
83
  Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
80
84
  }
81
85
 
@@ -155,7 +159,12 @@ impl Transaction {
155
159
  }
156
160
 
157
161
  /// Merge a value with options within the transaction.
158
- pub fn merge_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
162
+ pub fn merge_with_options(
163
+ &self,
164
+ key: String,
165
+ value: String,
166
+ kwargs: RHash,
167
+ ) -> Result<(), Error> {
159
168
  if key.is_empty() {
160
169
  return Err(invalid_argument_error("key cannot be empty"));
161
170
  }
@@ -244,6 +253,18 @@ impl Transaction {
244
253
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
245
254
  opts.max_fetch_tasks = mft;
246
255
  }
256
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
257
+ opts.order = match order.as_str() {
258
+ "ascending" | "asc" => IterationOrder::Ascending,
259
+ "descending" | "desc" => IterationOrder::Descending,
260
+ other => {
261
+ return Err(invalid_argument_error(&format!(
262
+ "invalid order: {} (expected 'asc' or 'desc')",
263
+ other
264
+ )))
265
+ }
266
+ };
267
+ }
247
268
 
248
269
  let guard = self.inner.borrow();
249
270
  let txn = guard
@@ -274,7 +295,7 @@ impl Transaction {
274
295
  .as_ref()
275
296
  .ok_or_else(|| closed_error("transaction is closed"))?;
276
297
 
277
- let iter = block_on_result(async { txn.scan_prefix(prefix.as_bytes()).await })?;
298
+ let iter = block_on_result(async { txn.scan_prefix(prefix.as_bytes(), ..).await })?;
278
299
 
279
300
  Ok(Iterator::new(iter))
280
301
  }
@@ -319,14 +340,30 @@ impl Transaction {
319
340
  if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
320
341
  opts.max_fetch_tasks = mft;
321
342
  }
343
+ if let Some(order) = get_optional::<String>(&kwargs, "order")? {
344
+ opts.order = match order.as_str() {
345
+ "ascending" | "asc" => IterationOrder::Ascending,
346
+ "descending" | "desc" => IterationOrder::Descending,
347
+ other => {
348
+ return Err(invalid_argument_error(&format!(
349
+ "invalid order: {} (expected 'asc' or 'desc')",
350
+ other
351
+ )))
352
+ }
353
+ };
354
+ }
355
+
356
+ let subrange = prefix_subrange_from_kwargs(&kwargs)?;
322
357
 
323
358
  let guard = self.inner.borrow();
324
359
  let txn = guard
325
360
  .as_ref()
326
361
  .ok_or_else(|| closed_error("transaction is closed"))?;
327
362
 
328
- let iter =
329
- block_on_result(async { txn.scan_prefix_with_options(prefix.as_bytes(), &opts).await })?;
363
+ let iter = block_on_result(async {
364
+ txn.scan_prefix_with_options(prefix.as_bytes(), subrange, &opts)
365
+ .await
366
+ })?;
330
367
 
331
368
  Ok(Iterator::new(iter))
332
369
  }
@@ -365,7 +402,11 @@ impl Transaction {
365
402
  /// Commit the transaction with options.
366
403
  pub fn commit_with_options(&self, kwargs: RHash) -> Result<(), Error> {
367
404
  let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
368
- let write_opts = WriteOptions { await_durable };
405
+ let seqnum = get_optional::<u64>(&kwargs, "seqnum")?.unwrap_or(0);
406
+ let write_opts = WriteOptions {
407
+ await_durable,
408
+ seqnum,
409
+ };
369
410
 
370
411
  let txn = self
371
412
  .inner
@@ -422,7 +463,7 @@ pub fn define_transaction_class(ruby: &Ruby, module: &magnus::RModule) -> Result
422
463
  method!(Transaction::scan_prefix_with_options, 2),
423
464
  )?;
424
465
  class.define_method("_mark_read", method!(Transaction::mark_read, 1))?;
425
- class.define_method("commit", method!(Transaction::commit, 0))?;
466
+ class.define_method("_commit", method!(Transaction::commit, 0))?;
426
467
  class.define_method(
427
468
  "_commit_with_options",
428
469
  method!(Transaction::commit_with_options, 1),
@@ -1,10 +1,14 @@
1
+ use std::ops::Bound;
1
2
  use std::sync::Arc;
2
3
 
3
4
  use magnus::value::ReprValue;
4
5
  use magnus::{Error, RHash, Ruby, TryConvert};
5
6
  use slatedb::object_store::aws::AmazonS3Builder;
6
- use slatedb::object_store::{Error as ObjectStoreError, ObjectStore, ObjectStoreScheme};
7
- use slatedb::{Db, Error as SlateError};
7
+ use slatedb::object_store::prefix::PrefixStore;
8
+ use slatedb::object_store::{
9
+ parse_url_opts, Error as ObjectStoreError, ObjectStore, ObjectStoreScheme,
10
+ };
11
+ use slatedb::Error as SlateError;
8
12
  use url::Url;
9
13
 
10
14
  /// Helper to extract an optional value from an RHash
@@ -23,6 +27,30 @@ pub fn get_optional<T: TryConvert>(hash: &RHash, key: &str) -> Result<Option<T>,
23
27
  }
24
28
  }
25
29
 
30
+ /// A key sub-range for a prefix scan, expressed as owned byte bounds. This
31
+ /// implements `slatedb::bytes_range::ByteRangeBounds`, so it can be passed
32
+ /// straight to `scan_prefix`/`scan_prefix_with_options`.
33
+ pub type PrefixSubrange = (Bound<Vec<u8>>, Bound<Vec<u8>>);
34
+
35
+ /// Build a suffix sub-range (relative to a scan prefix) from optional
36
+ /// `subrange_from` / `subrange_to` keyword arguments.
37
+ ///
38
+ /// The bounds are key *suffixes* appended to the prefix: `subrange_from` is an
39
+ /// inclusive lower bound and `subrange_to` is an exclusive upper bound. A
40
+ /// missing bound is unbounded, so an empty hash yields a full-prefix scan
41
+ /// (equivalent to `..`).
42
+ pub fn prefix_subrange_from_kwargs(hash: &RHash) -> Result<PrefixSubrange, Error> {
43
+ let start = match get_optional::<String>(hash, "subrange_from")? {
44
+ Some(s) => Bound::Included(s.into_bytes()),
45
+ None => Bound::Unbounded,
46
+ };
47
+ let end = match get_optional::<String>(hash, "subrange_to")? {
48
+ Some(s) => Bound::Excluded(s.into_bytes()),
49
+ None => Bound::Unbounded,
50
+ };
51
+ Ok((start, end))
52
+ }
53
+
26
54
  /// Convert an object_store error to a SlateDB error
27
55
  fn to_slate_error(e: ObjectStoreError) -> SlateError {
28
56
  SlateError::unavailable(e.to_string())
@@ -52,8 +80,22 @@ pub fn resolve_object_store(url: &str) -> Result<Arc<dyn ObjectStore>, SlateErro
52
80
  Ok(Arc::new(store))
53
81
  }
54
82
  _ => {
55
- // Fall back to slatedb's default resolver for other schemes
56
- Db::resolve_object_store(url)
83
+ // SlateDB 0.14 changed `Db::resolve_object_store` to reject any URL
84
+ // that carries a path component (returning `InvalidObjectStorePath`),
85
+ // whereas 0.13 transparently wrapped it in a `PrefixStore`. Preserve
86
+ // the old, more forgiving behavior here so callers can keep passing a
87
+ // full location such as "file:///data/mydb" or "gs://bucket/prefix".
88
+ //
89
+ // Env keys are lowercased because `parse_url_opts` only recognizes
90
+ // lower-case option keys.
91
+ let env_vars = std::env::vars().map(|(k, v)| (k.to_ascii_lowercase(), v));
92
+ let (store, path) = parse_url_opts(&parsed_url, env_vars).map_err(to_slate_error)?;
93
+ let store: Arc<dyn ObjectStore> = Arc::from(store);
94
+ if path.as_ref().is_empty() {
95
+ Ok(store)
96
+ } else {
97
+ Ok(Arc::new(PrefixStore::new(store, path)))
98
+ }
57
99
  }
58
100
  }
59
101
  }
@@ -89,7 +89,12 @@ impl WriteBatch {
89
89
  ///
90
90
  /// Options:
91
91
  /// - ttl: Time-to-live in milliseconds
92
- pub fn merge_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
92
+ pub fn merge_with_options(
93
+ &self,
94
+ key: String,
95
+ value: String,
96
+ kwargs: RHash,
97
+ ) -> Result<(), Error> {
93
98
  if key.is_empty() {
94
99
  return Err(invalid_argument_error("key cannot be empty"));
95
100
  }