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.
- checksums.yaml +7 -0
- data/Cargo.toml +9 -0
- data/LICENSE +176 -0
- data/README.md +404 -0
- data/ext/slatedb/Cargo.toml +22 -0
- data/ext/slatedb/extconf.rb +6 -0
- data/ext/slatedb/src/admin.rs +273 -0
- data/ext/slatedb/src/database.rs +457 -0
- data/ext/slatedb/src/errors.rs +144 -0
- data/ext/slatedb/src/iterator.rs +118 -0
- data/ext/slatedb/src/lib.rs +50 -0
- data/ext/slatedb/src/reader.rs +233 -0
- data/ext/slatedb/src/runtime.rs +78 -0
- data/ext/slatedb/src/snapshot.rs +197 -0
- data/ext/slatedb/src/transaction.rs +298 -0
- data/ext/slatedb/src/utils.rs +18 -0
- data/ext/slatedb/src/write_batch.rs +98 -0
- data/lib/slatedb/admin.rb +122 -0
- data/lib/slatedb/database.rb +310 -0
- data/lib/slatedb/iterator.rb +31 -0
- data/lib/slatedb/reader.rb +105 -0
- data/lib/slatedb/snapshot.rb +54 -0
- data/lib/slatedb/transaction.rb +78 -0
- data/lib/slatedb/version.rb +5 -0
- data/lib/slatedb/write_batch.rb +38 -0
- data/lib/slatedb.rb +20 -0
- metadata +140 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use magnus::prelude::*;
|
|
4
|
+
use magnus::{function, method, Error, RHash, Ruby};
|
|
5
|
+
use slatedb::config::{DurabilityLevel, PutOptions, ReadOptions, ScanOptions, Ttl, WriteOptions};
|
|
6
|
+
use slatedb::object_store::memory::InMemory;
|
|
7
|
+
use slatedb::{Db, IsolationLevel};
|
|
8
|
+
|
|
9
|
+
use crate::errors::{invalid_argument_error, map_error};
|
|
10
|
+
use crate::iterator::Iterator;
|
|
11
|
+
use crate::runtime::block_on;
|
|
12
|
+
use crate::snapshot::Snapshot;
|
|
13
|
+
use crate::transaction::Transaction;
|
|
14
|
+
use crate::utils::get_optional;
|
|
15
|
+
use crate::write_batch::WriteBatch;
|
|
16
|
+
|
|
17
|
+
/// Ruby wrapper for SlateDB database.
|
|
18
|
+
///
|
|
19
|
+
/// This struct is exposed to Ruby as `SlateDb::Database`.
|
|
20
|
+
#[magnus::wrap(class = "SlateDb::Database", free_immediately, size)]
|
|
21
|
+
pub struct Database {
|
|
22
|
+
inner: Arc<Db>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl Database {
|
|
26
|
+
/// Open a database at the given path.
|
|
27
|
+
///
|
|
28
|
+
/// # Arguments
|
|
29
|
+
/// * `path` - The path identifier for the database
|
|
30
|
+
/// * `url` - Optional object store URL (e.g., "s3://bucket/path")
|
|
31
|
+
///
|
|
32
|
+
/// # Returns
|
|
33
|
+
/// A new Database instance
|
|
34
|
+
pub fn open(path: String, url: Option<String>) -> Result<Self, Error> {
|
|
35
|
+
let db = block_on(async {
|
|
36
|
+
let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) = url {
|
|
37
|
+
Db::resolve_object_store(url).map_err(map_error)?
|
|
38
|
+
} else {
|
|
39
|
+
// Use in-memory store for local testing
|
|
40
|
+
Arc::new(InMemory::new())
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Db::builder(path, object_store)
|
|
44
|
+
.build()
|
|
45
|
+
.await
|
|
46
|
+
.map_err(map_error)
|
|
47
|
+
})?;
|
|
48
|
+
|
|
49
|
+
Ok(Self {
|
|
50
|
+
inner: Arc::new(db),
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Get a value by key.
|
|
55
|
+
///
|
|
56
|
+
/// # Arguments
|
|
57
|
+
/// * `key` - The key to look up
|
|
58
|
+
///
|
|
59
|
+
/// # Returns
|
|
60
|
+
/// The value as a String, or nil if not found
|
|
61
|
+
pub fn get(&self, key: String) -> Result<Option<String>, Error> {
|
|
62
|
+
if key.is_empty() {
|
|
63
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let opts = ReadOptions::default();
|
|
67
|
+
|
|
68
|
+
let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
|
|
69
|
+
.map_err(map_error)?;
|
|
70
|
+
|
|
71
|
+
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Get a value by key with options.
|
|
75
|
+
///
|
|
76
|
+
/// # Arguments
|
|
77
|
+
/// * `key` - The key to look up
|
|
78
|
+
/// * `kwargs` - Keyword arguments (durability_filter, dirty, cache_blocks)
|
|
79
|
+
///
|
|
80
|
+
/// # Returns
|
|
81
|
+
/// The value as a String, or nil if not found
|
|
82
|
+
pub fn get_with_options(&self, key: String, kwargs: RHash) -> Result<Option<String>, Error> {
|
|
83
|
+
if key.is_empty() {
|
|
84
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let mut opts = ReadOptions::default();
|
|
88
|
+
|
|
89
|
+
// Parse durability_filter
|
|
90
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
91
|
+
opts.durability_filter = match df.as_str() {
|
|
92
|
+
"remote" => DurabilityLevel::Remote,
|
|
93
|
+
"memory" => DurabilityLevel::Memory,
|
|
94
|
+
other => {
|
|
95
|
+
return Err(invalid_argument_error(&format!(
|
|
96
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
97
|
+
other
|
|
98
|
+
)))
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Parse dirty
|
|
104
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
105
|
+
opts.dirty = dirty;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
|
|
109
|
+
.map_err(map_error)?;
|
|
110
|
+
|
|
111
|
+
Ok(result.map(|b| String::from_utf8_lossy(&b).to_string()))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Get a value by key as raw bytes.
|
|
115
|
+
///
|
|
116
|
+
/// # Arguments
|
|
117
|
+
/// * `key` - The key to look up
|
|
118
|
+
///
|
|
119
|
+
/// # Returns
|
|
120
|
+
/// The value as bytes, or nil if not found
|
|
121
|
+
pub fn get_bytes(&self, key: String) -> Result<Option<Vec<u8>>, Error> {
|
|
122
|
+
if key.is_empty() {
|
|
123
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let opts = ReadOptions::default();
|
|
127
|
+
|
|
128
|
+
let result = block_on(async { self.inner.get_with_options(key.as_bytes(), &opts).await })
|
|
129
|
+
.map_err(map_error)?;
|
|
130
|
+
|
|
131
|
+
Ok(result.map(|b| b.to_vec()))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Store a key-value pair.
|
|
135
|
+
///
|
|
136
|
+
/// # Arguments
|
|
137
|
+
/// * `key` - The key to store
|
|
138
|
+
/// * `value` - The value to store
|
|
139
|
+
pub fn put(&self, key: String, value: String) -> Result<(), Error> {
|
|
140
|
+
if key.is_empty() {
|
|
141
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let put_opts = PutOptions { ttl: Ttl::Default };
|
|
145
|
+
|
|
146
|
+
let write_opts = WriteOptions {
|
|
147
|
+
await_durable: true,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
block_on(async {
|
|
151
|
+
self.inner
|
|
152
|
+
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
153
|
+
.await
|
|
154
|
+
})
|
|
155
|
+
.map_err(map_error)?;
|
|
156
|
+
|
|
157
|
+
Ok(())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Store a key-value pair with options.
|
|
161
|
+
///
|
|
162
|
+
/// # Arguments
|
|
163
|
+
/// * `key` - The key to store
|
|
164
|
+
/// * `value` - The value to store
|
|
165
|
+
/// * `kwargs` - Keyword arguments (ttl, await_durable)
|
|
166
|
+
pub fn put_with_options(&self, key: String, value: String, kwargs: RHash) -> Result<(), Error> {
|
|
167
|
+
if key.is_empty() {
|
|
168
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse ttl
|
|
172
|
+
let ttl = get_optional::<u64>(&kwargs, "ttl")?;
|
|
173
|
+
let put_opts = PutOptions {
|
|
174
|
+
ttl: match ttl {
|
|
175
|
+
Some(ms) => Ttl::ExpireAfter(ms),
|
|
176
|
+
None => Ttl::Default,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Parse await_durable
|
|
181
|
+
let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
|
|
182
|
+
let write_opts = WriteOptions { await_durable };
|
|
183
|
+
|
|
184
|
+
block_on(async {
|
|
185
|
+
self.inner
|
|
186
|
+
.put_with_options(key.as_bytes(), value.as_bytes(), &put_opts, &write_opts)
|
|
187
|
+
.await
|
|
188
|
+
})
|
|
189
|
+
.map_err(map_error)?;
|
|
190
|
+
|
|
191
|
+
Ok(())
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Delete a key.
|
|
195
|
+
///
|
|
196
|
+
/// # Arguments
|
|
197
|
+
/// * `key` - The key to delete
|
|
198
|
+
pub fn delete(&self, key: String) -> Result<(), Error> {
|
|
199
|
+
if key.is_empty() {
|
|
200
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let write_opts = WriteOptions {
|
|
204
|
+
await_durable: true,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
block_on(async {
|
|
208
|
+
self.inner
|
|
209
|
+
.delete_with_options(key.as_bytes(), &write_opts)
|
|
210
|
+
.await
|
|
211
|
+
})
|
|
212
|
+
.map_err(map_error)?;
|
|
213
|
+
|
|
214
|
+
Ok(())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Delete a key with options.
|
|
218
|
+
///
|
|
219
|
+
/// # Arguments
|
|
220
|
+
/// * `key` - The key to delete
|
|
221
|
+
/// * `kwargs` - Keyword arguments (await_durable)
|
|
222
|
+
pub fn delete_with_options(&self, key: String, kwargs: RHash) -> Result<(), Error> {
|
|
223
|
+
if key.is_empty() {
|
|
224
|
+
return Err(invalid_argument_error("key cannot be empty"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
|
|
228
|
+
let write_opts = WriteOptions { await_durable };
|
|
229
|
+
|
|
230
|
+
block_on(async {
|
|
231
|
+
self.inner
|
|
232
|
+
.delete_with_options(key.as_bytes(), &write_opts)
|
|
233
|
+
.await
|
|
234
|
+
})
|
|
235
|
+
.map_err(map_error)?;
|
|
236
|
+
|
|
237
|
+
Ok(())
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Scan a range of keys.
|
|
241
|
+
///
|
|
242
|
+
/// # Arguments
|
|
243
|
+
/// * `start` - The start key (inclusive)
|
|
244
|
+
/// * `end_key` - Optional end key (exclusive). If not provided, scans to end.
|
|
245
|
+
///
|
|
246
|
+
/// # Returns
|
|
247
|
+
/// An Iterator over key-value pairs
|
|
248
|
+
pub fn scan(&self, start: String, end_key: Option<String>) -> Result<Iterator, Error> {
|
|
249
|
+
if start.is_empty() {
|
|
250
|
+
return Err(invalid_argument_error("start key cannot be empty"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let opts = ScanOptions::default();
|
|
254
|
+
|
|
255
|
+
let start_bytes = start.into_bytes();
|
|
256
|
+
let end_bytes = end_key.map(|e| e.into_bytes());
|
|
257
|
+
|
|
258
|
+
let iter = block_on(async {
|
|
259
|
+
let range = match end_bytes {
|
|
260
|
+
Some(end) => self.inner.scan_with_options(start_bytes..end, &opts).await,
|
|
261
|
+
None => self.inner.scan_with_options(start_bytes.., &opts).await,
|
|
262
|
+
};
|
|
263
|
+
range.map_err(map_error)
|
|
264
|
+
})?;
|
|
265
|
+
|
|
266
|
+
Ok(Iterator::new(iter))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Scan a range of keys with options.
|
|
270
|
+
///
|
|
271
|
+
/// # Arguments
|
|
272
|
+
/// * `start` - The start key (inclusive)
|
|
273
|
+
/// * `end_key` - Optional end key (exclusive)
|
|
274
|
+
/// * `kwargs` - Keyword arguments (durability_filter, dirty, read_ahead_bytes, cache_blocks, max_fetch_tasks)
|
|
275
|
+
///
|
|
276
|
+
/// # Returns
|
|
277
|
+
/// An Iterator over key-value pairs
|
|
278
|
+
pub fn scan_with_options(
|
|
279
|
+
&self,
|
|
280
|
+
start: String,
|
|
281
|
+
end_key: Option<String>,
|
|
282
|
+
kwargs: RHash,
|
|
283
|
+
) -> Result<Iterator, Error> {
|
|
284
|
+
if start.is_empty() {
|
|
285
|
+
return Err(invalid_argument_error("start key cannot be empty"));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let mut opts = ScanOptions::default();
|
|
289
|
+
|
|
290
|
+
// Parse durability_filter
|
|
291
|
+
if let Some(df) = get_optional::<String>(&kwargs, "durability_filter")? {
|
|
292
|
+
opts.durability_filter = match df.as_str() {
|
|
293
|
+
"remote" => DurabilityLevel::Remote,
|
|
294
|
+
"memory" => DurabilityLevel::Memory,
|
|
295
|
+
other => {
|
|
296
|
+
return Err(invalid_argument_error(&format!(
|
|
297
|
+
"invalid durability_filter: {} (expected 'remote' or 'memory')",
|
|
298
|
+
other
|
|
299
|
+
)))
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Parse dirty
|
|
305
|
+
if let Some(dirty) = get_optional::<bool>(&kwargs, "dirty")? {
|
|
306
|
+
opts.dirty = dirty;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Parse read_ahead_bytes
|
|
310
|
+
if let Some(rab) = get_optional::<usize>(&kwargs, "read_ahead_bytes")? {
|
|
311
|
+
opts.read_ahead_bytes = rab;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Parse cache_blocks
|
|
315
|
+
if let Some(cb) = get_optional::<bool>(&kwargs, "cache_blocks")? {
|
|
316
|
+
opts.cache_blocks = cb;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Parse max_fetch_tasks
|
|
320
|
+
if let Some(mft) = get_optional::<usize>(&kwargs, "max_fetch_tasks")? {
|
|
321
|
+
opts.max_fetch_tasks = mft;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let start_bytes = start.into_bytes();
|
|
325
|
+
let end_bytes = end_key.map(|e| e.into_bytes());
|
|
326
|
+
|
|
327
|
+
let iter = block_on(async {
|
|
328
|
+
let range = match end_bytes {
|
|
329
|
+
Some(end) => self.inner.scan_with_options(start_bytes..end, &opts).await,
|
|
330
|
+
None => self.inner.scan_with_options(start_bytes.., &opts).await,
|
|
331
|
+
};
|
|
332
|
+
range.map_err(map_error)
|
|
333
|
+
})?;
|
|
334
|
+
|
|
335
|
+
Ok(Iterator::new(iter))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Write a batch of operations atomically.
|
|
339
|
+
///
|
|
340
|
+
/// # Arguments
|
|
341
|
+
/// * `batch` - The WriteBatch to write
|
|
342
|
+
pub fn write(&self, batch: &WriteBatch) -> Result<(), Error> {
|
|
343
|
+
let batch_inner = batch.take()?;
|
|
344
|
+
|
|
345
|
+
block_on(async { self.inner.write(batch_inner).await }).map_err(map_error)?;
|
|
346
|
+
|
|
347
|
+
Ok(())
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Write a batch of operations atomically with options.
|
|
351
|
+
///
|
|
352
|
+
/// # Arguments
|
|
353
|
+
/// * `batch` - The WriteBatch to write
|
|
354
|
+
/// * `kwargs` - Keyword arguments (await_durable)
|
|
355
|
+
pub fn write_with_options(&self, batch: &WriteBatch, kwargs: RHash) -> Result<(), Error> {
|
|
356
|
+
let await_durable = get_optional::<bool>(&kwargs, "await_durable")?.unwrap_or(true);
|
|
357
|
+
let write_opts = WriteOptions { await_durable };
|
|
358
|
+
|
|
359
|
+
let batch_inner = batch.take()?;
|
|
360
|
+
|
|
361
|
+
block_on(async {
|
|
362
|
+
self.inner
|
|
363
|
+
.write_with_options(batch_inner, &write_opts)
|
|
364
|
+
.await
|
|
365
|
+
})
|
|
366
|
+
.map_err(map_error)?;
|
|
367
|
+
|
|
368
|
+
Ok(())
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// Begin a new transaction.
|
|
372
|
+
///
|
|
373
|
+
/// # Arguments
|
|
374
|
+
/// * `isolation` - Optional isolation level ("snapshot" or "serializable")
|
|
375
|
+
///
|
|
376
|
+
/// # Returns
|
|
377
|
+
/// A new Transaction instance
|
|
378
|
+
pub fn begin_transaction(&self, isolation: Option<String>) -> Result<Transaction, Error> {
|
|
379
|
+
let isolation_level = match isolation.as_deref().unwrap_or("snapshot") {
|
|
380
|
+
"snapshot" | "si" => IsolationLevel::Snapshot,
|
|
381
|
+
"serializable" | "ssi" | "serializable_snapshot" => {
|
|
382
|
+
IsolationLevel::SerializableSnapshot
|
|
383
|
+
}
|
|
384
|
+
other => {
|
|
385
|
+
return Err(invalid_argument_error(&format!(
|
|
386
|
+
"invalid isolation level: {} (expected 'snapshot' or 'serializable')",
|
|
387
|
+
other
|
|
388
|
+
)))
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
let txn = block_on(async { self.inner.begin(isolation_level).await }).map_err(map_error)?;
|
|
393
|
+
|
|
394
|
+
Ok(Transaction::new(txn))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// Create a snapshot for consistent reads.
|
|
398
|
+
///
|
|
399
|
+
/// # Returns
|
|
400
|
+
/// A new Snapshot instance
|
|
401
|
+
pub fn snapshot(&self) -> Result<Snapshot, Error> {
|
|
402
|
+
let snap = block_on(async { self.inner.snapshot().await }).map_err(map_error)?;
|
|
403
|
+
|
|
404
|
+
Ok(Snapshot::new(snap))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Flush the database to ensure durability.
|
|
408
|
+
pub fn flush(&self) -> Result<(), Error> {
|
|
409
|
+
block_on(async { self.inner.flush().await }).map_err(map_error)?;
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/// Close the database.
|
|
414
|
+
pub fn close(&self) -> Result<(), Error> {
|
|
415
|
+
block_on(async { self.inner.close().await }).map_err(map_error)?;
|
|
416
|
+
Ok(())
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// Define the Database class on the SlateDb module.
|
|
421
|
+
pub fn define_database_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
422
|
+
let class = module.define_class("Database", ruby.class_object())?;
|
|
423
|
+
|
|
424
|
+
// Class methods
|
|
425
|
+
class.define_singleton_method("_open", function!(Database::open, 2))?;
|
|
426
|
+
|
|
427
|
+
// Instance methods - simple versions
|
|
428
|
+
class.define_method("_get", method!(Database::get, 1))?;
|
|
429
|
+
class.define_method("_get_with_options", method!(Database::get_with_options, 2))?;
|
|
430
|
+
class.define_method("get_bytes", method!(Database::get_bytes, 1))?;
|
|
431
|
+
class.define_method("_put", method!(Database::put, 2))?;
|
|
432
|
+
class.define_method("_put_with_options", method!(Database::put_with_options, 3))?;
|
|
433
|
+
class.define_method("_delete", method!(Database::delete, 1))?;
|
|
434
|
+
class.define_method(
|
|
435
|
+
"_delete_with_options",
|
|
436
|
+
method!(Database::delete_with_options, 2),
|
|
437
|
+
)?;
|
|
438
|
+
class.define_method("_scan", method!(Database::scan, 2))?;
|
|
439
|
+
class.define_method(
|
|
440
|
+
"_scan_with_options",
|
|
441
|
+
method!(Database::scan_with_options, 3),
|
|
442
|
+
)?;
|
|
443
|
+
class.define_method("_write", method!(Database::write, 1))?;
|
|
444
|
+
class.define_method(
|
|
445
|
+
"_write_with_options",
|
|
446
|
+
method!(Database::write_with_options, 2),
|
|
447
|
+
)?;
|
|
448
|
+
class.define_method(
|
|
449
|
+
"_begin_transaction",
|
|
450
|
+
method!(Database::begin_transaction, 1),
|
|
451
|
+
)?;
|
|
452
|
+
class.define_method("_snapshot", method!(Database::snapshot, 0))?;
|
|
453
|
+
class.define_method("flush", method!(Database::flush, 0))?;
|
|
454
|
+
class.define_method("close", method!(Database::close, 0))?;
|
|
455
|
+
|
|
456
|
+
Ok(())
|
|
457
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
use magnus::prelude::*;
|
|
2
|
+
use magnus::{Error, ExceptionClass, Ruby};
|
|
3
|
+
use slatedb::Error as SlateError;
|
|
4
|
+
use slatedb::ErrorKind;
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
|
|
7
|
+
// Store exception classes in thread-local storage for access during error mapping
|
|
8
|
+
thread_local! {
|
|
9
|
+
static SLATE_DB_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
10
|
+
static TRANSACTION_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
11
|
+
static CLOSED_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
12
|
+
static UNAVAILABLE_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
13
|
+
static INVALID_ARGUMENT_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
14
|
+
static DATA_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
15
|
+
static INTERNAL_ERROR: RefCell<Option<ExceptionClass>> = const { RefCell::new(None) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Define SlateDB exception classes under the SlateDb module.
|
|
19
|
+
///
|
|
20
|
+
/// Exception hierarchy:
|
|
21
|
+
/// - SlateDb::Error (base class, inherits from StandardError)
|
|
22
|
+
/// - SlateDb::TransactionError
|
|
23
|
+
/// - SlateDb::ClosedError
|
|
24
|
+
/// - SlateDb::UnavailableError
|
|
25
|
+
/// - SlateDb::InvalidArgumentError
|
|
26
|
+
/// - SlateDb::DataError
|
|
27
|
+
/// - SlateDb::InternalError
|
|
28
|
+
pub fn define_exceptions(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
29
|
+
let standard_error = ruby.exception_standard_error();
|
|
30
|
+
|
|
31
|
+
// Define base SlateDb::Error
|
|
32
|
+
let slate_error = module.define_error("Error", standard_error)?;
|
|
33
|
+
SLATE_DB_ERROR.with(|cell| {
|
|
34
|
+
*cell.borrow_mut() = Some(slate_error);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Define specific error types
|
|
38
|
+
let transaction_error = module.define_error("TransactionError", slate_error)?;
|
|
39
|
+
TRANSACTION_ERROR.with(|cell| {
|
|
40
|
+
*cell.borrow_mut() = Some(transaction_error);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let closed_error = module.define_error("ClosedError", slate_error)?;
|
|
44
|
+
CLOSED_ERROR.with(|cell| {
|
|
45
|
+
*cell.borrow_mut() = Some(closed_error);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let unavailable_error = module.define_error("UnavailableError", slate_error)?;
|
|
49
|
+
UNAVAILABLE_ERROR.with(|cell| {
|
|
50
|
+
*cell.borrow_mut() = Some(unavailable_error);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let invalid_argument_error = module.define_error("InvalidArgumentError", slate_error)?;
|
|
54
|
+
INVALID_ARGUMENT_ERROR.with(|cell| {
|
|
55
|
+
*cell.borrow_mut() = Some(invalid_argument_error);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let data_error = module.define_error("DataError", slate_error)?;
|
|
59
|
+
DATA_ERROR.with(|cell| {
|
|
60
|
+
*cell.borrow_mut() = Some(data_error);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let internal_error = module.define_error("InternalError", slate_error)?;
|
|
64
|
+
INTERNAL_ERROR.with(|cell| {
|
|
65
|
+
*cell.borrow_mut() = Some(internal_error);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Map a SlateDB error to the appropriate Ruby exception.
|
|
72
|
+
pub fn map_error(err: SlateError) -> Error {
|
|
73
|
+
let msg = format!("{}", err);
|
|
74
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
75
|
+
|
|
76
|
+
match err.kind() {
|
|
77
|
+
ErrorKind::Transaction => TRANSACTION_ERROR.with(|cell| {
|
|
78
|
+
cell.borrow()
|
|
79
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
80
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
81
|
+
}),
|
|
82
|
+
ErrorKind::Closed(_) => CLOSED_ERROR.with(|cell| {
|
|
83
|
+
cell.borrow()
|
|
84
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
85
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
86
|
+
}),
|
|
87
|
+
ErrorKind::Unavailable => UNAVAILABLE_ERROR.with(|cell| {
|
|
88
|
+
cell.borrow()
|
|
89
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
90
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
91
|
+
}),
|
|
92
|
+
ErrorKind::Invalid => INVALID_ARGUMENT_ERROR.with(|cell| {
|
|
93
|
+
cell.borrow()
|
|
94
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
95
|
+
.unwrap_or_else(|| Error::new(ruby.exception_arg_error(), msg.clone()))
|
|
96
|
+
}),
|
|
97
|
+
ErrorKind::Data => DATA_ERROR.with(|cell| {
|
|
98
|
+
cell.borrow()
|
|
99
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
100
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
101
|
+
}),
|
|
102
|
+
ErrorKind::Internal => INTERNAL_ERROR.with(|cell| {
|
|
103
|
+
cell.borrow()
|
|
104
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
105
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
106
|
+
}),
|
|
107
|
+
_ => INTERNAL_ERROR.with(|cell| {
|
|
108
|
+
cell.borrow()
|
|
109
|
+
.map(|exc| Error::new(exc, msg.clone()))
|
|
110
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.clone()))
|
|
111
|
+
}),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Create an InvalidArgumentError with the given message.
|
|
116
|
+
pub fn invalid_argument_error(msg: &str) -> Error {
|
|
117
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
118
|
+
INVALID_ARGUMENT_ERROR.with(|cell| {
|
|
119
|
+
cell.borrow()
|
|
120
|
+
.map(|exc| Error::new(exc, msg.to_string()))
|
|
121
|
+
.unwrap_or_else(|| Error::new(ruby.exception_arg_error(), msg.to_string()))
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Create an InternalError with the given message.
|
|
126
|
+
#[allow(dead_code)]
|
|
127
|
+
pub fn internal_error(msg: &str) -> Error {
|
|
128
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
129
|
+
INTERNAL_ERROR.with(|cell| {
|
|
130
|
+
cell.borrow()
|
|
131
|
+
.map(|exc| Error::new(exc, msg.to_string()))
|
|
132
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.to_string()))
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Create a ClosedError with the given message.
|
|
137
|
+
pub fn closed_error(msg: &str) -> Error {
|
|
138
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
139
|
+
CLOSED_ERROR.with(|cell| {
|
|
140
|
+
cell.borrow()
|
|
141
|
+
.map(|exc| Error::new(exc, msg.to_string()))
|
|
142
|
+
.unwrap_or_else(|| Error::new(ruby.exception_runtime_error(), msg.to_string()))
|
|
143
|
+
})
|
|
144
|
+
}
|