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,298 @@
1
+ use std::cell::RefCell;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{method, Error, RHash, Ruby};
5
+ use slatedb::config::{DurabilityLevel, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions};
6
+ use slatedb::DBTransaction;
7
+
8
+ use crate::errors::{closed_error, 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 Transaction.
14
+ ///
15
+ /// This struct is exposed to Ruby as `SlateDb::Transaction`.
16
+ /// After commit or rollback, the transaction is closed.
17
+ #[magnus::wrap(class = "SlateDb::Transaction", free_immediately, size)]
18
+ pub struct Transaction {
19
+ inner: RefCell<Option<DBTransaction>>,
20
+ }
21
+
22
+ impl Transaction {
23
+ /// Create a new Transaction from a DBTransaction.
24
+ pub fn new(txn: DBTransaction) -> Self {
25
+ Self {
26
+ inner: RefCell::new(Some(txn)),
27
+ }
28
+ }
29
+
30
+ /// Get a value by key within the transaction.
31
+ pub fn get(&self, key: String) -> Result<Option<String>, Error> {
32
+ if key.is_empty() {
33
+ return Err(invalid_argument_error("key cannot be empty"));
34
+ }
35
+
36
+ let guard = self.inner.borrow();
37
+ let txn = guard
38
+ .as_ref()
39
+ .ok_or_else(|| closed_error("transaction is closed"))?;
40
+
41
+ let result = block_on(async { txn.get(key.as_bytes()).await }).map_err(map_error)?;
42
+
43
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
44
+ }
45
+
46
+ /// Get a value by key with options within the transaction.
47
+ pub fn get_with_options(&self, key: String, kwargs: RHash) -> Result<Option<String>, Error> {
48
+ if key.is_empty() {
49
+ return Err(invalid_argument_error("key cannot be empty"));
50
+ }
51
+
52
+ let mut opts = ReadOptions::default();
53
+
54
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
55
+ opts.durability_filter = match df.as_str() {
56
+ "remote" => DurabilityLevel::Remote,
57
+ "memory" => DurabilityLevel::Memory,
58
+ other => {
59
+ return Err(invalid_argument_error(&format!(
60
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
61
+ other
62
+ )))
63
+ }
64
+ };
65
+ }
66
+
67
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
68
+ opts.dirty = dirty;
69
+ }
70
+
71
+ let guard = self.inner.borrow();
72
+ let txn = guard
73
+ .as_ref()
74
+ .ok_or_else(|| closed_error("transaction is closed"))?;
75
+
76
+ let result = block_on(async { txn.get_with_options(key.as_bytes(), &opts).await })
77
+ .map_err(map_error)?;
78
+
79
+ Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
80
+ }
81
+
82
+ /// Put a key-value pair within the transaction.
83
+ pub fn put(&self, key: String, value: String) -> Result<(), Error> {
84
+ if key.is_empty() {
85
+ return Err(invalid_argument_error("key cannot be empty"));
86
+ }
87
+
88
+ let guard = self.inner.borrow();
89
+ let txn = guard
90
+ .as_ref()
91
+ .ok_or_else(|| closed_error("transaction is closed"))?;
92
+
93
+ txn.put(key.as_bytes(), value.as_bytes())
94
+ .map_err(map_error)?;
95
+
96
+ Ok(())
97
+ }
98
+
99
+ /// Put a key-value pair with options within the transaction.
100
+ pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
101
+ if key.is_empty() {
102
+ return Err(invalid_argument_error("key cannot be empty"));
103
+ }
104
+
105
+ let ttl = get_optional::<u64>(&kwargs, "ttl")?;
106
+ let put_opts = PutOptions {
107
+ ttl: match ttl {
108
+ Some(ms) => Ttl::ExpireAfter(ms),
109
+ None => Ttl::Default,
110
+ },
111
+ };
112
+
113
+ let guard = self.inner.borrow();
114
+ let txn = guard
115
+ .as_ref()
116
+ .ok_or_else(|| closed_error("transaction is closed"))?;
117
+
118
+ txn.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts)
119
+ .map_err(map_error)?;
120
+
121
+ Ok(())
122
+ }
123
+
124
+ /// Delete a key within the transaction.
125
+ pub fn delete(&self, key: String) -> Result<(), Error> {
126
+ if key.is_empty() {
127
+ return Err(invalid_argument_error("key cannot be empty"));
128
+ }
129
+
130
+ let guard = self.inner.borrow();
131
+ let txn = guard
132
+ .as_ref()
133
+ .ok_or_else(|| closed_error("transaction is closed"))?;
134
+
135
+ txn.delete(key.as_bytes()).map_err(map_error)?;
136
+
137
+ Ok(())
138
+ }
139
+
140
+ /// Scan a range of keys within the transaction.
141
+ pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
142
+ if start.is_empty() {
143
+ return Err(invalid_argument_error("start key cannot be empty"));
144
+ }
145
+
146
+ let guard = self.inner.borrow();
147
+ let txn = guard
148
+ .as_ref()
149
+ .ok_or_else(|| closed_error("transaction is closed"))?;
150
+
151
+ let start_bytes = start.into_bytes();
152
+ let end_bytes = end_key.map(|e| e.into_bytes());
153
+
154
+ let iter = block_on(async {
155
+ let range = match end_bytes {
156
+ Some(end) => txn.scan(start_bytes..end).await,
157
+ None => txn.scan(start_bytes..).await,
158
+ };
159
+ range.map_err(map_error)
160
+ })?;
161
+
162
+ Ok(Iterator::new(iter))
163
+ }
164
+
165
+ /// Scan a range of keys with options within the transaction.
166
+ pub fn scan_with_options(
167
+ &self,
168
+ start: String,
169
+ end_key: Option<String>,
170
+ kwargs: RHash,
171
+ ) -> Result<Iterator, Error> {
172
+ if start.is_empty() {
173
+ return Err(invalid_argument_error("start key cannot be empty"));
174
+ }
175
+
176
+ let mut opts = ScanOptions::default();
177
+
178
+ if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
179
+ opts.durability_filter = match df.as_str() {
180
+ "remote" => DurabilityLevel::Remote,
181
+ "memory" => DurabilityLevel::Memory,
182
+ other => {
183
+ return Err(invalid_argument_error(&format!(
184
+ "invalid durability_filter: {} (expected 'remote' or 'memory')",
185
+ other
186
+ )))
187
+ }
188
+ };
189
+ }
190
+
191
+ if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
192
+ opts.dirty = dirty;
193
+ }
194
+
195
+ if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
196
+ opts.read_ahead_bytes = rab;
197
+ }
198
+
199
+ if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
200
+ opts.cache_blocks = cb;
201
+ }
202
+
203
+ if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
204
+ opts.max_fetch_tasks = mft;
205
+ }
206
+
207
+ let guard = self.inner.borrow();
208
+ let txn = guard
209
+ .as_ref()
210
+ .ok_or_else(|| closed_error("transaction is closed"))?;
211
+
212
+ let start_bytes = start.into_bytes();
213
+ let end_bytes = end_key.map(|e| e.into_bytes());
214
+
215
+ let iter = block_on(async {
216
+ let range = match end_bytes {
217
+ Some(end) => txn.scan_with_options(start_bytes..end, &opts).await,
218
+ None => txn.scan_with_options(start_bytes.., &opts).await,
219
+ };
220
+ range.map_err(map_error)
221
+ })?;
222
+
223
+ Ok(Iterator::new(iter))
224
+ }
225
+
226
+ /// Commit the transaction.
227
+ pub fn commit(&self) -> Result<(), Error> {
228
+ let txn = self
229
+ .inner
230
+ .borrow_mut()
231
+ .take()
232
+ .ok_or_else(|| closed_error("transaction is closed"))?;
233
+
234
+ block_on(async { txn.commit().await }).map_err(map_error)?;
235
+
236
+ Ok(())
237
+ }
238
+
239
+ /// Commit the transaction with options.
240
+ pub fn commit_with_options(&self, kwargs: RHash) -> Result<(), Error> {
241
+ let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
242
+ let write_opts = WriteOptions { await_durable };
243
+
244
+ let txn = self
245
+ .inner
246
+ .borrow_mut()
247
+ .take()
248
+ .ok_or_else(|| closed_error("transaction is closed"))?;
249
+
250
+ block_on(async { txn.commit_with_options(&write_opts).await }).map_err(map_error)?;
251
+
252
+ Ok(())
253
+ }
254
+
255
+ /// Rollback the transaction (discard all changes).
256
+ pub fn rollback(&self) -> Result<(), Error> {
257
+ // Simply drop the transaction - changes are not committed
258
+ let _ = self.inner.borrow_mut().take();
259
+ Ok(())
260
+ }
261
+
262
+ /// Check if the transaction is closed.
263
+ pub fn is_closed(&self) -> bool {
264
+ self.inner.borrow().is_none()
265
+ }
266
+ }
267
+
268
+ /// Define the Transaction class on the SlateDb module.
269
+ pub fn define_transaction_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
270
+ let class = module.define_class("Transaction", ruby.class_object())?;
271
+
272
+ // Instance methods
273
+ class.define_method("_get", method!(Transaction::get, 1))?;
274
+ class.define_method(
275
+ "_get_with_options",
276
+ method!(Transaction::get_with_options, 2),
277
+ )?;
278
+ class.define_method("_put", method!(Transaction::put, 2))?;
279
+ class.define_method(
280
+ "_put_with_options",
281
+ method!(Transaction::put_with_options, 3),
282
+ )?;
283
+ class.define_method("_delete", method!(Transaction::delete, 1))?;
284
+ class.define_method("_scan", method!(Transaction::scan, 2))?;
285
+ class.define_method(
286
+ "_scan_with_options",
287
+ method!(Transaction::scan_with_options, 3),
288
+ )?;
289
+ class.define_method("commit", method!(Transaction::commit, 0))?;
290
+ class.define_method(
291
+ "_commit_with_options",
292
+ method!(Transaction::commit_with_options, 1),
293
+ )?;
294
+ class.define_method("rollback", method!(Transaction::rollback, 0))?;
295
+ class.define_method("closed?", method!(Transaction::is_closed, 0))?;
296
+
297
+ Ok(())
298
+ }
@@ -0,0 +1,18 @@
1
+ use magnus::value::ReprValue;
2
+ use magnus::{Error, RHash, Ruby, TryConvert};
3
+
4
+ /// Helper to extract an optional value from an RHash
5
+ pub fn get_optional<T: TryConvert>(hash: &RHash, key: &str) -> Result<Option<T>, Error> {
6
+ let ruby = Ruby::get().expect("Ruby runtime not available");
7
+ let sym = ruby.to_symbol(key);
8
+ match hash.get(sym) {
9
+ Some(val) => {
10
+ if val.is_nil() {
11
+ Ok(None)
12
+ } else {
13
+ Ok(Some(T::try_convert(val)?))
14
+ }
15
+ }
16
+ None => Ok(None),
17
+ }
18
+ }
@@ -0,0 +1,98 @@
1
+ use std::cell::RefCell;
2
+
3
+ use magnus::prelude::*;
4
+ use magnus::{function, method, Error, RHash, Ruby};
5
+ use slatedb::config::{PutOptions, Ttl};
6
+ use slatedb::WriteBatch as SlateWriteBatch;
7
+
8
+ use crate::errors::invalid_argument_error;
9
+ use crate::utils::get_optional;
10
+
11
+ /// Ruby wrapper for SlateDB WriteBatch.
12
+ ///
13
+ /// This struct is exposed to Ruby as `SlateDb::WriteBatch`.
14
+ #[magnus::wrap(class = "SlateDb::WriteBatch", free_immediately, size)]
15
+ pub struct WriteBatch {
16
+ inner: RefCell<SlateWriteBatch>,
17
+ }
18
+
19
+ impl WriteBatch {
20
+ /// Create a new empty WriteBatch.
21
+ pub fn new() -> Self {
22
+ Self {
23
+ inner: RefCell::new(SlateWriteBatch::new()),
24
+ }
25
+ }
26
+
27
+ /// Add a put operation to the batch.
28
+ pub fn put(&self, key: String, value: String) -> Result<(), Error> {
29
+ if key.is_empty() {
30
+ return Err(invalid_argument_error("key cannot be empty"));
31
+ }
32
+
33
+ self.inner
34
+ .borrow_mut()
35
+ .put(key.as_bytes(), value.as_bytes());
36
+
37
+ Ok(())
38
+ }
39
+
40
+ /// Add a put operation with options to the batch.
41
+ ///
42
+ /// Options:
43
+ /// - ttl: Time-to-live in milliseconds
44
+ pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
45
+ if key.is_empty() {
46
+ return Err(invalid_argument_error("key cannot be empty"));
47
+ }
48
+
49
+ let ttl = get_optional::<u64>(&kwargs, "ttl")?;
50
+ let put_opts = PutOptions {
51
+ ttl: match ttl {
52
+ Some(ms) => Ttl::ExpireAfter(ms),
53
+ None => Ttl::Default,
54
+ },
55
+ };
56
+
57
+ self.inner
58
+ .borrow_mut()
59
+ .put_with_options(key.as_bytes(), value.as_bytes(), &put_opts);
60
+
61
+ Ok(())
62
+ }
63
+
64
+ /// Add a delete operation to the batch.
65
+ pub fn delete(&self, key: String) -> Result<(), Error> {
66
+ if key.is_empty() {
67
+ return Err(invalid_argument_error("key cannot be empty"));
68
+ }
69
+
70
+ self.inner.borrow_mut().delete(key.as_bytes());
71
+
72
+ Ok(())
73
+ }
74
+
75
+ /// Take ownership of the inner WriteBatch (consumes it).
76
+ /// Used internally when writing the batch to the database.
77
+ pub fn take(&self) -> Result<SlateWriteBatch, Error> {
78
+ Ok(self.inner.replace(SlateWriteBatch::new()))
79
+ }
80
+ }
81
+
82
+ /// Define the WriteBatch class on the SlateDb module.
83
+ pub fn define_write_batch_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
84
+ let class = module.define_class("WriteBatch", ruby.class_object())?;
85
+
86
+ // Class methods
87
+ class.define_singleton_method("new", function!(WriteBatch::new, 0))?;
88
+
89
+ // Instance methods
90
+ class.define_method("_put", method!(WriteBatch::put, 2))?;
91
+ class.define_method(
92
+ "_put_with_options",
93
+ method!(WriteBatch::put_with_options, 3),
94
+ )?;
95
+ class.define_method("_delete", method!(WriteBatch::delete, 1))?;
96
+
97
+ Ok(())
98
+ }
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class Admin
5
+ class << self
6
+ # Create an admin handle for a database path/object store.
7
+ #
8
+ # @param path [String] Database path
9
+ # @param url [String, nil] Optional object store URL
10
+ # @return [Admin] The admin handle
11
+ #
12
+ # @example
13
+ # admin = SlateDb::Admin.new("/tmp/mydb")
14
+ # checkpoints = admin.list_checkpoints
15
+ #
16
+ def new(path, url: nil)
17
+ _new(path, url)
18
+ end
19
+ end
20
+
21
+ # Read the latest or a specific manifest as a JSON string.
22
+ #
23
+ # @param id [Integer, nil] Optional manifest id to read. If nil, reads the latest.
24
+ # @return [String, nil] JSON string of the manifest, or nil if no manifests exist
25
+ #
26
+ # @example
27
+ # json = admin.read_manifest
28
+ # json = admin.read_manifest(123)
29
+ #
30
+ def read_manifest(id = nil)
31
+ _read_manifest(id)
32
+ end
33
+
34
+ # List manifests within an optional [start, end) range as JSON.
35
+ #
36
+ # @param start [Integer, nil] Optional inclusive start id
37
+ # @param end_id [Integer, nil] Optional exclusive end id
38
+ # @return [String] JSON string containing a list of manifest metadata
39
+ #
40
+ # @example
41
+ # json = admin.list_manifests
42
+ # json = admin.list_manifests(start: 1, end_id: 10)
43
+ #
44
+ def list_manifests(start: nil, end_id: nil)
45
+ _list_manifests(start, end_id)
46
+ end
47
+
48
+ # Create a detached checkpoint.
49
+ #
50
+ # @param lifetime [Integer, nil] Checkpoint lifetime in milliseconds
51
+ # @param source [String, nil] Source checkpoint UUID string to extend/refresh
52
+ # @param name [String, nil] Checkpoint name
53
+ # @return [Hash] Hash with :id (UUID string) and :manifest_id (Integer)
54
+ #
55
+ # @example
56
+ # result = admin.create_checkpoint(name: "my_checkpoint")
57
+ # puts result[:id] # => "uuid-string"
58
+ # puts result[:manifest_id] # => 7
59
+ #
60
+ def create_checkpoint(lifetime: nil, source: nil, name: nil)
61
+ opts = {}
62
+ opts[:lifetime] = lifetime if lifetime
63
+ opts[:source] = source if source
64
+ opts[:name] = name if name
65
+ _create_checkpoint(opts)
66
+ end
67
+
68
+ # List known checkpoints for the database.
69
+ #
70
+ # @param name [String, nil] Optional checkpoint name filter
71
+ # @return [Array<Hash>] Array of checkpoint hashes
72
+ #
73
+ # @example
74
+ # checkpoints = admin.list_checkpoints
75
+ # checkpoints.each do |cp|
76
+ # puts "#{cp[:id]}: #{cp[:name]}"
77
+ # end
78
+ #
79
+ def list_checkpoints(name: nil)
80
+ _list_checkpoints(name)
81
+ end
82
+
83
+ # Refresh a checkpoint's lifetime.
84
+ #
85
+ # @param id [String] Checkpoint UUID string
86
+ # @param lifetime [Integer, nil] New lifetime in milliseconds
87
+ # @return [void]
88
+ #
89
+ # @example
90
+ # admin.refresh_checkpoint("uuid-here", lifetime: 60_000)
91
+ #
92
+ def refresh_checkpoint(id, lifetime: nil)
93
+ _refresh_checkpoint(id, lifetime)
94
+ end
95
+
96
+ # Delete a checkpoint.
97
+ #
98
+ # @param id [String] Checkpoint UUID string
99
+ # @return [void]
100
+ #
101
+ # @example
102
+ # admin.delete_checkpoint("uuid-here")
103
+ #
104
+ def delete_checkpoint(id)
105
+ _delete_checkpoint(id)
106
+ end
107
+
108
+ # Run garbage collection once.
109
+ #
110
+ # @param min_age [Integer, nil] Minimum age in milliseconds for objects to be collected
111
+ # @return [void]
112
+ #
113
+ # @example
114
+ # admin.run_gc(min_age: 3600_000) # 1 hour
115
+ #
116
+ def run_gc(min_age: nil)
117
+ opts = {}
118
+ opts[:min_age] = min_age if min_age
119
+ _run_gc(opts)
120
+ end
121
+ end
122
+ end