slatedb 0.1.0

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.
@@ -0,0 +1,118 @@
1
+ use std::sync::Arc;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{method, Error, Ruby};
5
+ use slatedb::DbIterator;
6
+ use tokio::sync::Mutex;
7
+
8
+ /// Result type for raw byte key-value pairs.
9
+ type ByteKvResult = Result<Option<(Vec<u8>, Vec<u8>)>, Error>;
10
+
11
+ use crate::errors::{internal_error, invalid_argument_error, map_error};
12
+ use crate::runtime::block_on;
13
+
14
+ /// Ruby wrapper for SlateDB iterator.
15
+ ///
16
+ /// This struct is exposed to Ruby as `SlateDb::Iterator`.
17
+ /// It includes Enumerable support via the `each` method implemented in Ruby.
18
+ #[magnus::wrap(class = "SlateDb::Iterator", free_immediately, size)]
19
+ pub struct Iterator {
20
+ inner: Arc<Mutex<Option<DbIterator>>>,
21
+ }
22
+
23
+ impl Iterator {
24
+ /// Create a new Iterator from a DbIterator.
25
+ pub fn new(iter: DbIterator) -> Self {
26
+ Self {
27
+ inner: Arc::new(Mutex::new(Some(iter))),
28
+ }
29
+ }
30
+
31
+ /// Get the next key-value pair.
32
+ ///
33
+ /// Returns [key, value] as an array, or nil if iteration is complete.
34
+ pub fn next_entry(&self) -> Result<Option<(String, String)>, Error> {
35
+ let inner = self.inner.clone();
36
+
37
+ let result = block_on(async {
38
+ let mut guard = inner.lock().await;
39
+ let iter = guard
40
+ .as_mut()
41
+ .ok_or_else(|| internal_error("iterator has been closed"))?;
42
+
43
+ iter.next().await.map_err(map_error)
44
+ })?;
45
+
46
+ Ok(result.map(|kv| {
47
+ (
48
+ String::from_utf8_lossy(&kv.key).to_string(),
49
+ String::from_utf8_lossy(&kv.value).to_string(),
50
+ )
51
+ }))
52
+ }
53
+
54
+ /// Get the next key-value pair as raw bytes.
55
+ ///
56
+ /// Returns [key, value] as byte arrays, or nil if iteration is complete.
57
+ pub fn next_entry_bytes(&self) -> ByteKvResult {
58
+ let inner = self.inner.clone();
59
+
60
+ let result = block_on(async {
61
+ let mut guard = inner.lock().await;
62
+ let iter = guard
63
+ .as_mut()
64
+ .ok_or_else(|| internal_error("iterator has been closed"))?;
65
+
66
+ iter.next().await.map_err(map_error)
67
+ })?;
68
+
69
+ Ok(result.map(|kv| (kv.key.to_vec(), kv.value.to_vec())))
70
+ }
71
+
72
+ /// Seek to a specific key position.
73
+ ///
74
+ /// After seeking, `next` will return entries starting from the given key.
75
+ pub fn seek(&self, key: String) -> Result<(), Error> {
76
+ if key.is_empty() {
77
+ return Err(invalid_argument_error("key cannot be empty"));
78
+ }
79
+
80
+ let inner = self.inner.clone();
81
+
82
+ block_on(async {
83
+ let mut guard = inner.lock().await;
84
+ let iter = guard
85
+ .as_mut()
86
+ .ok_or_else(|| internal_error("iterator has been closed"))?;
87
+
88
+ iter.seek(key.as_bytes()).await.map_err(map_error)
89
+ })?;
90
+
91
+ Ok(())
92
+ }
93
+
94
+ /// Close the iterator and release resources.
95
+ pub fn close(&self) -> Result<(), Error> {
96
+ let inner = self.inner.clone();
97
+
98
+ block_on(async {
99
+ let mut guard = inner.lock().await;
100
+ *guard = None;
101
+ });
102
+
103
+ Ok(())
104
+ }
105
+ }
106
+
107
+ /// Define the Iterator class on the SlateDb module.
108
+ pub fn define_iterator_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
109
+ let class = module.define_class("Iterator", ruby.class_object())?;
110
+
111
+ // Instance methods
112
+ class.define_method("next_entry", method!(Iterator::next_entry, 0))?;
113
+ class.define_method("next_entry_bytes", method!(Iterator::next_entry_bytes, 0))?;
114
+ class.define_method("seek", method!(Iterator::seek, 1))?;
115
+ class.define_method("close", method!(Iterator::close, 0))?;
116
+
117
+ Ok(())
118
+ }
@@ -0,0 +1,50 @@
1
+ //! SlateDB Ruby bindings
2
+ //!
3
+ //! This crate provides Ruby bindings for SlateDB, a cloud-native embedded
4
+ //! key-value store built on object storage.
5
+ //!
6
+ //! # Example
7
+ //!
8
+ //! ```ruby
9
+ //! require 'slatedb'
10
+ //!
11
+ //! db = SlateDb::Database.open("/tmp/mydb")
12
+ //! db.put("hello", "world")
13
+ //! db.get("hello") # => "world"
14
+ //! db.close
15
+ //! ```
16
+
17
+ use magnus::{Error, Ruby};
18
+
19
+ mod admin;
20
+ mod database;
21
+ mod errors;
22
+ mod iterator;
23
+ mod reader;
24
+ mod runtime;
25
+ mod snapshot;
26
+ mod transaction;
27
+ mod utils;
28
+ mod write_batch;
29
+
30
+ /// Initialize the SlateDb Ruby module.
31
+ ///
32
+ /// This is called automatically when the native extension is loaded.
33
+ #[magnus::init]
34
+ fn init(ruby: &Ruby) -> Result<(), Error> {
35
+ let module = ruby.define_module("SlateDb")?;
36
+
37
+ // Define exception classes first
38
+ errors::define_exceptions(ruby, &module)?;
39
+
40
+ // Define core classes
41
+ database::define_database_class(ruby, &module)?;
42
+ iterator::define_iterator_class(ruby, &module)?;
43
+ write_batch::define_write_batch_class(ruby, &module)?;
44
+ transaction::define_transaction_class(ruby, &module)?;
45
+ snapshot::define_snapshot_class(ruby, &module)?;
46
+ reader::define_reader_class(ruby, &module)?;
47
+ admin::define_admin_class(ruby, &module)?;
48
+
49
+ Ok(())
50
+ }
@@ -0,0 +1,233 @@
1
+ use std::sync::Arc;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{function, method, Error, RHash, Ruby};
5
+ use slatedb::config::{DbReaderOptions, DurabilityLevel, ReadOptions, ScanOptions};
6
+ use slatedb::DbReader;
7
+
8
+ use crate::errors::{invalid_argument_error, map_error};
9
+ use crate::iterator::Iterator;
10
+ use crate::runtime::block_on;
11
+ use crate::utils::get_optional;
12
+
13
+ /// Ruby wrapper for SlateDB Reader.
14
+ ///
15
+ /// This struct is exposed to Ruby as `SlateDb::Reader`.
16
+ /// Provides read-only access to a database, optionally pinned to a checkpoint.
17
+ #[magnus::wrap(class = "SlateDb::Reader", free_immediately, size)]
18
+ pub struct Reader {
19
+ inner: Arc<DbReader>,
20
+ }
21
+
22
+ impl Reader {
23
+ /// Open a reader at the given path.
24
+ ///
25
+ /// # Arguments
26
+ /// * `path` - The path identifier for the database
27
+ /// * `url` - Optional object store URL
28
+ /// * `checkpoint_id` - Optional checkpoint UUID to read at
29
+ /// * `kwargs` - Additional options (manifest_poll_interval, checkpoint_lifetime, max_memtable_bytes)
30
+ pub fn open(
31
+ path: String,
32
+ url: Option<String>,
33
+ checkpoint_id: Option<String>,
34
+ kwargs: RHash,
35
+ ) -> Result<Self, Error> {
36
+ // Parse options
37
+ let manifest_poll_interval = get_optional::<u64>(&kwargs, "manifest_poll_interval")?
38
+ .map(std::time::Duration::from_millis);
39
+ let checkpoint_lifetime = get_optional::<u64>(&kwargs, "checkpoint_lifetime")?
40
+ .map(std::time::Duration::from_millis);
41
+ let max_memtable_bytes = get_optional::<u64>(&kwargs, "max_memtable_bytes")?;
42
+
43
+ // Parse checkpoint_id as UUID
44
+ let checkpoint_uuid =
45
+ if let Some(id_str) = checkpoint_id {
46
+ Some(uuid::Uuid::parse_str(&id_str).map_err(|e| {
47
+ invalid_argument_error(&format!("invalid checkpoint_id: {}", e))
48
+ })?)
49
+ } else {
50
+ None
51
+ };
52
+
53
+ let reader = block_on(async {
54
+ let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) = url {
55
+ slatedb::Db::resolve_object_store(url).map_err(map_error)?
56
+ } else {
57
+ Arc::new(object_store::memory::InMemory::new())
58
+ };
59
+
60
+ let mut options = DbReaderOptions::default();
61
+ if let Some(interval) = manifest_poll_interval {
62
+ options.manifest_poll_interval = interval;
63
+ }
64
+ if let Some(lifetime) = checkpoint_lifetime {
65
+ options.checkpoint_lifetime = lifetime;
66
+ }
67
+ if let Some(max_bytes) = max_memtable_bytes {
68
+ options.max_memtable_bytes = max_bytes;
69
+ }
70
+
71
+ DbReader::open(path, object_store, checkpoint_uuid, options)
72
+ .await
73
+ .map_err(map_error)
74
+ })?;
75
+
76
+ Ok(Self {
77
+ inner: Arc::new(reader),
78
+ })
79
+ }
80
+
81
+ /// Get a value by key.
82
+ pub fn get(&self, key: String) -> Result<Option<String>, Error> {
83
+ if key.is_empty() {
84
+ return Err(invalid_argument_error("key cannot be empty"));
85
+ }
86
+
87
+ let result = block_on(async { self.inner.get(key.as_bytes()).await }).map_err(map_error)?;
88
+
89
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
90
+ }
91
+
92
+ /// Get a value by key with options.
93
+ pub fn get_with_options(&self, key: String, kwargs: RHash) -> Result<Option<String>, Error> {
94
+ if key.is_empty() {
95
+ return Err(invalid_argument_error("key cannot be empty"));
96
+ }
97
+
98
+ let mut opts = ReadOptions::default();
99
+
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
+ };
111
+ }
112
+
113
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
114
+ opts.dirty = dirty;
115
+ }
116
+
117
+ let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
118
+ .map_err(map_error)?;
119
+
120
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
121
+ }
122
+
123
+ /// Get a value by key as raw bytes.
124
+ pub fn get_bytes(&self, key: String) -> Result<Option<Vec<u8>>, Error> {
125
+ if key.is_empty() {
126
+ return Err(invalid_argument_error("key cannot be empty"));
127
+ }
128
+
129
+ let result = block_on(async { self.inner.get(key.as_bytes()).await }).map_err(map_error)?;
130
+
131
+ Ok(result.map(|b| b.to_vec()))
132
+ }
133
+
134
+ /// Scan a range of keys.
135
+ pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
136
+ if start.is_empty() {
137
+ return Err(invalid_argument_error("start key cannot be empty"));
138
+ }
139
+
140
+ let start_bytes = start.into_bytes();
141
+ let end_bytes = end_key.map(|e| e.into_bytes());
142
+
143
+ let iter = block_on(async {
144
+ let range = match end_bytes {
145
+ Some(end) => self.inner.scan(start_bytes..end).await,
146
+ None => self.inner.scan(start_bytes..).await,
147
+ };
148
+ range.map_err(map_error)
149
+ })?;
150
+
151
+ Ok(Iterator::new(iter))
152
+ }
153
+
154
+ /// Scan a range of keys with options.
155
+ pub fn scan_with_options(
156
+ &self,
157
+ start: String,
158
+ end_key: Option<String>,
159
+ kwargs: RHash,
160
+ ) -> Result<Iterator, Error> {
161
+ if start.is_empty() {
162
+ return Err(invalid_argument_error("start key cannot be empty"));
163
+ }
164
+
165
+ let mut opts = ScanOptions::default();
166
+
167
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
168
+ opts.durability_filter = match df.as_str() {
169
+ "remote" => DurabilityLevel::Remote,
170
+ "memory" => DurabilityLevel::Memory,
171
+ other => {
172
+ return Err(invalid_argument_error(&format!(
173
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
174
+ other
175
+ )))
176
+ }
177
+ };
178
+ }
179
+
180
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
181
+ opts.dirty = dirty;
182
+ }
183
+
184
+ if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
185
+ opts.read_ahead_bytes = rab;
186
+ }
187
+
188
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
189
+ opts.cache_blocks = cb;
190
+ }
191
+
192
+ if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
193
+ opts.max_fetch_tasks = mft;
194
+ }
195
+
196
+ let start_bytes = start.into_bytes();
197
+ let end_bytes = end_key.map(|e| e.into_bytes());
198
+
199
+ let iter = block_on(async {
200
+ let range = match end_bytes {
201
+ Some(end) => self.inner.scan_with_options(start_bytes..end, &opts).await,
202
+ None => self.inner.scan_with_options(start_bytes.., &opts).await,
203
+ };
204
+ range.map_err(map_error)
205
+ })?;
206
+
207
+ Ok(Iterator::new(iter))
208
+ }
209
+
210
+ /// Close the reader.
211
+ pub fn close(&self) -> Result<(), Error> {
212
+ block_on(async { self.inner.close().await }).map_err(map_error)?;
213
+ Ok(())
214
+ }
215
+ }
216
+
217
+ /// Define the Reader class on the SlateDb module.
218
+ pub fn define_reader_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
219
+ let class = module.define_class("Reader", ruby.class_object())?;
220
+
221
+ // Class methods
222
+ class.define_singleton_method("_open", function!(Reader::open, 4))?;
223
+
224
+ // Instance methods
225
+ class.define_method("_get", method!(Reader::get, 1))?;
226
+ class.define_method("_get_with_options", method!(Reader::get_with_options, 2))?;
227
+ class.define_method("get_bytes", method!(Reader::get_bytes, 1))?;
228
+ class.define_method("_scan", method!(Reader::scan, 2))?;
229
+ class.define_method("_scan_with_options", method!(Reader::scan_with_options, 3))?;
230
+ class.define_method("close", method!(Reader::close, 0))?;
231
+
232
+ Ok(())
233
+ }
@@ -0,0 +1,78 @@
1
+ use once_cell::sync::OnceCell;
2
+ use rb_sys::rb_thread_call_without_gvl;
3
+ use std::ffi::c_void;
4
+ use std::future::Future;
5
+ use tokio::runtime::Runtime;
6
+
7
+ static RUNTIME: OnceCell<Runtime> = OnceCell::new();
8
+
9
+ /// Get or initialize the shared Tokio runtime for all SlateDB operations.
10
+ ///
11
+ /// We use a multi-threaded runtime to support concurrent access from multiple
12
+ /// Ruby threads. This is important for use with Sidekiq, Puma, and other
13
+ /// multi-threaded Ruby applications.
14
+ fn get_runtime() -> &'static Runtime {
15
+ RUNTIME.get_or_init(|| {
16
+ tokio::runtime::Builder::new_multi_thread()
17
+ .enable_all()
18
+ .build()
19
+ .expect("Failed to create Tokio runtime")
20
+ })
21
+ }
22
+
23
+ /// Execute a future on the runtime, releasing the Ruby GVL while waiting.
24
+ ///
25
+ /// This is critical for thread safety - it allows other Ruby threads to run
26
+ /// while this thread waits for I/O operations to complete. Without this,
27
+ /// multiple Ruby threads calling SlateDB operations would deadlock.
28
+ pub fn block_on<F, T>(future: F) -> T
29
+ where
30
+ F: Future<Output = T>,
31
+ {
32
+ let rt = get_runtime();
33
+
34
+ // Use rb_thread_call_without_gvl to release the GVL while blocking
35
+ // This allows other Ruby threads to execute while we wait for I/O
36
+ without_gvl(|| rt.block_on(future))
37
+ }
38
+
39
+ /// Execute a closure without holding the Ruby GVL.
40
+ ///
41
+ /// This releases the Global VM Lock, allowing other Ruby threads to run
42
+ /// while this closure executes. Essential for I/O-bound operations.
43
+ fn without_gvl<F, T>(f: F) -> T
44
+ where
45
+ F: FnOnce() -> T,
46
+ {
47
+ struct Closure<F, T> {
48
+ f: Option<F>,
49
+ result: Option<T>,
50
+ }
51
+
52
+ extern "C" fn call_closure<F, T>(data: *mut c_void) -> *mut c_void
53
+ where
54
+ F: FnOnce() -> T,
55
+ {
56
+ let closure = unsafe { &mut *(data as *mut Closure<F, T>) };
57
+ if let Some(f) = closure.f.take() {
58
+ closure.result = Some(f());
59
+ }
60
+ std::ptr::null_mut()
61
+ }
62
+
63
+ let mut closure = Closure {
64
+ f: Some(f),
65
+ result: None,
66
+ };
67
+
68
+ unsafe {
69
+ rb_thread_call_without_gvl(
70
+ Some(call_closure::<F, T>),
71
+ &mut closure as *mut _ as *mut c_void,
72
+ None,
73
+ std::ptr::null_mut(),
74
+ );
75
+ }
76
+
77
+ closure.result.expect("closure did not run")
78
+ }
@@ -0,0 +1,197 @@
1
+ use std::cell::RefCell;
2
+ use std::sync::Arc;
3
+
4
+ use magnus::prelude::*;
5
+ use magnus::{method, Error, RHash, Ruby};
6
+ use slatedb::config::{DurabilityLevel, ReadOptions, ScanOptions};
7
+ use slatedb::DbSnapshot;
8
+
9
+ use crate::errors::{closed_error, invalid_argument_error, map_error};
10
+ use crate::iterator::Iterator;
11
+ use crate::runtime::block_on;
12
+ use crate::utils::get_optional;
13
+
14
+ /// Ruby wrapper for SlateDB Snapshot.
15
+ ///
16
+ /// This struct is exposed to Ruby as `SlateDb::Snapshot`.
17
+ /// Provides a consistent, read-only view of the database at a point in time.
18
+ #[magnus::wrap(class = "SlateDb::Snapshot", free_immediately, size)]
19
+ pub struct Snapshot {
20
+ inner: RefCell<Option<Arc<DbSnapshot>>>,
21
+ }
22
+
23
+ impl Snapshot {
24
+ /// Create a new Snapshot from a DbSnapshot.
25
+ pub fn new(snapshot: Arc<DbSnapshot>) -> Self {
26
+ Self {
27
+ inner: RefCell::new(Some(snapshot)),
28
+ }
29
+ }
30
+
31
+ /// Get a value by key from the snapshot.
32
+ pub fn get(&self, key: String) -> Result<Option<String>, Error> {
33
+ if key.is_empty() {
34
+ return Err(invalid_argument_error("key cannot be empty"));
35
+ }
36
+
37
+ let guard = self.inner.borrow();
38
+ let snapshot = guard
39
+ .as_ref()
40
+ .ok_or_else(|| closed_error("snapshot is closed"))?;
41
+
42
+ let result = block_on(async { snapshot.get(key.as_bytes()).await }).map_err(map_error)?;
43
+
44
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
45
+ }
46
+
47
+ /// Get a value by key with options from the snapshot.
48
+ pub fn get_with_options(&self, key: String, kwargs: RHash) -> Result<Option<String>, Error> {
49
+ if key.is_empty() {
50
+ return Err(invalid_argument_error("key cannot be empty"));
51
+ }
52
+
53
+ let mut opts = ReadOptions::default();
54
+
55
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
56
+ opts.durability_filter = match df.as_str() {
57
+ "remote" => DurabilityLevel::Remote,
58
+ "memory" => DurabilityLevel::Memory,
59
+ other => {
60
+ return Err(invalid_argument_error(&format!(
61
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
62
+ other
63
+ )))
64
+ }
65
+ };
66
+ }
67
+
68
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
69
+ opts.dirty = dirty;
70
+ }
71
+
72
+ let guard = self.inner.borrow();
73
+ let snapshot = guard
74
+ .as_ref()
75
+ .ok_or_else(|| closed_error("snapshot is closed"))?;
76
+
77
+ let result = block_on(async { snapshot.get_with_options(key.as_bytes(), &opts).await })
78
+ .map_err(map_error)?;
79
+
80
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
81
+ }
82
+
83
+ /// Scan a range of keys from the snapshot.
84
+ pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
85
+ if start.is_empty() {
86
+ return Err(invalid_argument_error("start key cannot be empty"));
87
+ }
88
+
89
+ let guard = self.inner.borrow();
90
+ let snapshot = guard
91
+ .as_ref()
92
+ .ok_or_else(|| closed_error("snapshot is closed"))?;
93
+
94
+ let start_bytes = start.into_bytes();
95
+ let end_bytes = end_key.map(|e| e.into_bytes());
96
+
97
+ let iter = block_on(async {
98
+ let range = match end_bytes {
99
+ Some(end) => snapshot.scan(start_bytes..end).await,
100
+ None => snapshot.scan(start_bytes..).await,
101
+ };
102
+ range.map_err(map_error)
103
+ })?;
104
+
105
+ Ok(Iterator::new(iter))
106
+ }
107
+
108
+ /// Scan a range of keys with options from the snapshot.
109
+ pub fn scan_with_options(
110
+ &self,
111
+ start: String,
112
+ end_key: Option<String>,
113
+ kwargs: RHash,
114
+ ) -> Result<Iterator, Error> {
115
+ if start.is_empty() {
116
+ return Err(invalid_argument_error("start key cannot be empty"));
117
+ }
118
+
119
+ let mut opts = ScanOptions::default();
120
+
121
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
122
+ opts.durability_filter = match df.as_str() {
123
+ "remote" => DurabilityLevel::Remote,
124
+ "memory" => DurabilityLevel::Memory,
125
+ other => {
126
+ return Err(invalid_argument_error(&format!(
127
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
128
+ other
129
+ )))
130
+ }
131
+ };
132
+ }
133
+
134
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
135
+ opts.dirty = dirty;
136
+ }
137
+
138
+ if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
139
+ opts.read_ahead_bytes = rab;
140
+ }
141
+
142
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
143
+ opts.cache_blocks = cb;
144
+ }
145
+
146
+ if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
147
+ opts.max_fetch_tasks = mft;
148
+ }
149
+
150
+ let guard = self.inner.borrow();
151
+ let snapshot = guard
152
+ .as_ref()
153
+ .ok_or_else(|| closed_error("snapshot is closed"))?;
154
+
155
+ let start_bytes = start.into_bytes();
156
+ let end_bytes = end_key.map(|e| e.into_bytes());
157
+
158
+ let iter = block_on(async {
159
+ let range = match end_bytes {
160
+ Some(end) => snapshot.scan_with_options(start_bytes..end, &opts).await,
161
+ None => snapshot.scan_with_options(start_bytes.., &opts).await,
162
+ };
163
+ range.map_err(map_error)
164
+ })?;
165
+
166
+ Ok(Iterator::new(iter))
167
+ }
168
+
169
+ /// Close the snapshot and release resources.
170
+ pub fn close(&self) -> Result<(), Error> {
171
+ let _ = self.inner.borrow_mut().take();
172
+ Ok(())
173
+ }
174
+
175
+ /// Check if the snapshot is closed.
176
+ pub fn is_closed(&self) -> bool {
177
+ self.inner.borrow().is_none()
178
+ }
179
+ }
180
+
181
+ /// Define the Snapshot class on the SlateDb module.
182
+ pub fn define_snapshot_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
183
+ let class = module.define_class("Snapshot", ruby.class_object())?;
184
+
185
+ // Instance methods
186
+ class.define_method("_get", method!(Snapshot::get, 1))?;
187
+ class.define_method("_get_with_options", method!(Snapshot::get_with_options, 2))?;
188
+ class.define_method("_scan", method!(Snapshot::scan, 2))?;
189
+ class.define_method(
190
+ "_scan_with_options",
191
+ method!(Snapshot::scan_with_options, 3),
192
+ )?;
193
+ class.define_method("close", method!(Snapshot::close, 0))?;
194
+ class.define_method("closed?", method!(Snapshot::is_closed, 0))?;
195
+
196
+ Ok(())
197
+ }