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,273 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use magnus::prelude::*;
|
|
4
|
+
use magnus::{function, method, Error, RHash, Ruby};
|
|
5
|
+
use slatedb::admin::AdminBuilder;
|
|
6
|
+
use slatedb::config::{CheckpointOptions, GarbageCollectorOptions};
|
|
7
|
+
|
|
8
|
+
use crate::errors::{invalid_argument_error, map_error};
|
|
9
|
+
use crate::runtime::block_on;
|
|
10
|
+
use crate::utils::get_optional;
|
|
11
|
+
|
|
12
|
+
/// Ruby wrapper for SlateDB Admin.
|
|
13
|
+
///
|
|
14
|
+
/// This struct is exposed to Ruby as `SlateDb::Admin`.
|
|
15
|
+
/// Provides administrative functions for managing manifests, checkpoints, and GC.
|
|
16
|
+
#[magnus::wrap(class = "SlateDb::Admin", free_immediately, size)]
|
|
17
|
+
pub struct Admin {
|
|
18
|
+
inner: slatedb::admin::Admin,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Admin {
|
|
22
|
+
/// Create an admin handle for a database path/object store.
|
|
23
|
+
///
|
|
24
|
+
/// # Arguments
|
|
25
|
+
/// * `path` - The path identifier for the database
|
|
26
|
+
/// * `url` - Optional object store URL
|
|
27
|
+
pub fn new(path: String, url: Option<String>) -> Result<Self, Error> {
|
|
28
|
+
let admin = block_on(async {
|
|
29
|
+
let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) = url {
|
|
30
|
+
slatedb::Db::resolve_object_store(url).map_err(map_error)?
|
|
31
|
+
} else {
|
|
32
|
+
Arc::new(object_store::memory::InMemory::new())
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
Ok::<_, Error>(AdminBuilder::new(path, object_store).build())
|
|
36
|
+
})?;
|
|
37
|
+
|
|
38
|
+
Ok(Self { inner: admin })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Read the latest or a specific manifest as a JSON string.
|
|
42
|
+
///
|
|
43
|
+
/// # Arguments
|
|
44
|
+
/// * `id` - Optional manifest id to read. If None, reads the latest.
|
|
45
|
+
///
|
|
46
|
+
/// # Returns
|
|
47
|
+
/// JSON string of the manifest, or None if no manifests exist.
|
|
48
|
+
pub fn read_manifest(&self, id: Option<u64>) -> Result<Option<String>, Error> {
|
|
49
|
+
block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
|
|
50
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
51
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// List manifests within an optional [start, end) range as JSON.
|
|
56
|
+
///
|
|
57
|
+
/// # Arguments
|
|
58
|
+
/// * `start` - Optional inclusive start id
|
|
59
|
+
/// * `end_id` - Optional exclusive end id
|
|
60
|
+
///
|
|
61
|
+
/// # Returns
|
|
62
|
+
/// JSON string containing a list of manifest metadata.
|
|
63
|
+
pub fn list_manifests(&self, start: Option<u64>, end_id: Option<u64>) -> Result<String, Error> {
|
|
64
|
+
let range = match (start, end_id) {
|
|
65
|
+
(Some(s), Some(e)) => s..e,
|
|
66
|
+
(Some(s), None) => s..u64::MAX,
|
|
67
|
+
(None, Some(e)) => 0..e,
|
|
68
|
+
(None, None) => 0..u64::MAX,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
|
|
72
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
73
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Create a detached checkpoint.
|
|
78
|
+
///
|
|
79
|
+
/// # Arguments
|
|
80
|
+
/// * `kwargs` - Options: lifetime (ms), source (UUID string), name
|
|
81
|
+
///
|
|
82
|
+
/// # Returns
|
|
83
|
+
/// Hash with id (UUID string) and manifest_id (int)
|
|
84
|
+
pub fn create_checkpoint(&self, kwargs: RHash) -> Result<RHash, Error> {
|
|
85
|
+
let lifetime =
|
|
86
|
+
get_optional::<u64>(&kwargs, "lifetime")?.map(std::time::Duration::from_millis);
|
|
87
|
+
let source = get_optional::<String>(&kwargs, "source")?;
|
|
88
|
+
let name = get_optional::<String>(&kwargs, "name")?;
|
|
89
|
+
|
|
90
|
+
// Parse source UUID if provided
|
|
91
|
+
let source_uuid = if let Some(ref s) = source {
|
|
92
|
+
Some(
|
|
93
|
+
uuid::Uuid::parse_str(s)
|
|
94
|
+
.map_err(|e| invalid_argument_error(&format!("invalid source UUID: {}", e)))?,
|
|
95
|
+
)
|
|
96
|
+
} else {
|
|
97
|
+
None
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let options = CheckpointOptions {
|
|
101
|
+
lifetime,
|
|
102
|
+
source: source_uuid,
|
|
103
|
+
name,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let result = block_on(async { self.inner.create_detached_checkpoint(&options).await })
|
|
107
|
+
.map_err(map_error)?;
|
|
108
|
+
|
|
109
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
110
|
+
let hash = ruby.hash_new();
|
|
111
|
+
hash.aset(ruby.to_symbol("id"), result.id.to_string())?;
|
|
112
|
+
hash.aset(ruby.to_symbol("manifest_id"), result.manifest_id)?;
|
|
113
|
+
|
|
114
|
+
Ok(hash)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// List known checkpoints for the database.
|
|
118
|
+
///
|
|
119
|
+
/// # Arguments
|
|
120
|
+
/// * `name` - Optional checkpoint name filter
|
|
121
|
+
///
|
|
122
|
+
/// # Returns
|
|
123
|
+
/// Array of checkpoint hashes
|
|
124
|
+
pub fn list_checkpoints(&self, name: Option<String>) -> Result<magnus::RArray, Error> {
|
|
125
|
+
let checkpoints = block_on(async { self.inner.list_checkpoints(name.as_deref()).await })
|
|
126
|
+
.map_err(|e| {
|
|
127
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
128
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
129
|
+
})?;
|
|
130
|
+
|
|
131
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
132
|
+
let result = ruby.ary_new_capa(checkpoints.len());
|
|
133
|
+
|
|
134
|
+
for cp in checkpoints {
|
|
135
|
+
let hash = ruby.hash_new();
|
|
136
|
+
hash.aset(ruby.to_symbol("id"), cp.id.to_string())?;
|
|
137
|
+
hash.aset(ruby.to_symbol("manifest_id"), cp.manifest_id)?;
|
|
138
|
+
hash.aset(
|
|
139
|
+
ruby.to_symbol("expire_time"),
|
|
140
|
+
cp.expire_time.map(|t| t.to_rfc3339()),
|
|
141
|
+
)?;
|
|
142
|
+
hash.aset(ruby.to_symbol("create_time"), cp.create_time.to_rfc3339())?;
|
|
143
|
+
hash.aset(ruby.to_symbol("name"), cp.name)?;
|
|
144
|
+
result.push(hash)?;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Ok(result)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Refresh a checkpoint's lifetime.
|
|
151
|
+
///
|
|
152
|
+
/// # Arguments
|
|
153
|
+
/// * `id` - Checkpoint UUID string
|
|
154
|
+
/// * `lifetime` - Optional new lifetime in milliseconds
|
|
155
|
+
pub fn refresh_checkpoint(&self, id: String, lifetime: Option<u64>) -> Result<(), Error> {
|
|
156
|
+
let checkpoint_uuid = uuid::Uuid::parse_str(&id)
|
|
157
|
+
.map_err(|e| invalid_argument_error(&format!("invalid checkpoint UUID: {}", e)))?;
|
|
158
|
+
|
|
159
|
+
let lifetime_duration = lifetime.map(std::time::Duration::from_millis);
|
|
160
|
+
|
|
161
|
+
block_on(async {
|
|
162
|
+
self.inner
|
|
163
|
+
.refresh_checkpoint(checkpoint_uuid, lifetime_duration)
|
|
164
|
+
.await
|
|
165
|
+
})
|
|
166
|
+
.map_err(map_error)?;
|
|
167
|
+
|
|
168
|
+
Ok(())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Delete a checkpoint.
|
|
172
|
+
///
|
|
173
|
+
/// # Arguments
|
|
174
|
+
/// * `id` - Checkpoint UUID string
|
|
175
|
+
pub fn delete_checkpoint(&self, id: String) -> Result<(), Error> {
|
|
176
|
+
let checkpoint_uuid = uuid::Uuid::parse_str(&id)
|
|
177
|
+
.map_err(|e| invalid_argument_error(&format!("invalid checkpoint UUID: {}", e)))?;
|
|
178
|
+
|
|
179
|
+
block_on(async { self.inner.delete_checkpoint(checkpoint_uuid).await })
|
|
180
|
+
.map_err(map_error)?;
|
|
181
|
+
|
|
182
|
+
Ok(())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// Run garbage collection once.
|
|
186
|
+
///
|
|
187
|
+
/// # Arguments
|
|
188
|
+
/// * `kwargs` - GC options:
|
|
189
|
+
/// - `min_age`: Minimum age in milliseconds for all directories (applies to manifest, wal, compacted)
|
|
190
|
+
/// - `manifest_min_age`: Specific minimum age in milliseconds for manifest directory
|
|
191
|
+
/// - `wal_min_age`: Specific minimum age in milliseconds for WAL directory
|
|
192
|
+
/// - `compacted_min_age`: Specific minimum age in milliseconds for compacted directory
|
|
193
|
+
///
|
|
194
|
+
/// If `min_age` is provided, it will be used for all directories unless a specific override is provided.
|
|
195
|
+
/// If no options are provided, defaults are used (manifest: 1 day, wal: 1 minute, compacted: 1 minute).
|
|
196
|
+
pub fn run_gc(&self, kwargs: RHash) -> Result<(), Error> {
|
|
197
|
+
use slatedb::config::GarbageCollectorDirectoryOptions;
|
|
198
|
+
|
|
199
|
+
// Extract options from kwargs
|
|
200
|
+
let min_age = get_optional::<u64>(&kwargs, "min_age")?;
|
|
201
|
+
let manifest_min_age = get_optional::<u64>(&kwargs, "manifest_min_age")?;
|
|
202
|
+
let wal_min_age = get_optional::<u64>(&kwargs, "wal_min_age")?;
|
|
203
|
+
let compacted_min_age = get_optional::<u64>(&kwargs, "compacted_min_age")?;
|
|
204
|
+
|
|
205
|
+
// Build GC options
|
|
206
|
+
let gc_opts = if min_age.is_none()
|
|
207
|
+
&& manifest_min_age.is_none()
|
|
208
|
+
&& wal_min_age.is_none()
|
|
209
|
+
&& compacted_min_age.is_none()
|
|
210
|
+
{
|
|
211
|
+
// No options provided, use defaults
|
|
212
|
+
GarbageCollectorOptions::default()
|
|
213
|
+
} else {
|
|
214
|
+
let default_opts = GarbageCollectorOptions::default();
|
|
215
|
+
|
|
216
|
+
// Helper to create directory options with custom min_age
|
|
217
|
+
let make_dir_opts =
|
|
218
|
+
|specific_age: Option<u64>,
|
|
219
|
+
fallback_age: Option<u64>,
|
|
220
|
+
default_opts: Option<GarbageCollectorDirectoryOptions>| {
|
|
221
|
+
let age_ms = specific_age.or(fallback_age);
|
|
222
|
+
if let Some(ms) = age_ms {
|
|
223
|
+
Some(GarbageCollectorDirectoryOptions {
|
|
224
|
+
interval: default_opts.as_ref().and_then(|o| o.interval),
|
|
225
|
+
min_age: std::time::Duration::from_millis(ms),
|
|
226
|
+
})
|
|
227
|
+
} else {
|
|
228
|
+
default_opts
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
GarbageCollectorOptions {
|
|
233
|
+
manifest_options: make_dir_opts(
|
|
234
|
+
manifest_min_age,
|
|
235
|
+
min_age,
|
|
236
|
+
default_opts.manifest_options,
|
|
237
|
+
),
|
|
238
|
+
wal_options: make_dir_opts(wal_min_age, min_age, default_opts.wal_options),
|
|
239
|
+
compacted_options: make_dir_opts(
|
|
240
|
+
compacted_min_age,
|
|
241
|
+
min_age,
|
|
242
|
+
default_opts.compacted_options,
|
|
243
|
+
),
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
block_on(async { self.inner.run_gc_once(gc_opts).await }).map_err(|e| {
|
|
248
|
+
let ruby = Ruby::get().expect("Ruby runtime not available");
|
|
249
|
+
Error::new(ruby.exception_runtime_error(), format!("{}", e))
|
|
250
|
+
})?;
|
|
251
|
+
|
|
252
|
+
Ok(())
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Define the Admin class on the SlateDb module.
|
|
257
|
+
pub fn define_admin_class(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
258
|
+
let class = module.define_class("Admin", ruby.class_object())?;
|
|
259
|
+
|
|
260
|
+
// Class methods
|
|
261
|
+
class.define_singleton_method("_new", function!(Admin::new, 2))?;
|
|
262
|
+
|
|
263
|
+
// Instance methods
|
|
264
|
+
class.define_method("_read_manifest", method!(Admin::read_manifest, 1))?;
|
|
265
|
+
class.define_method("_list_manifests", method!(Admin::list_manifests, 2))?;
|
|
266
|
+
class.define_method("_create_checkpoint", method!(Admin::create_checkpoint, 1))?;
|
|
267
|
+
class.define_method("_list_checkpoints", method!(Admin::list_checkpoints, 1))?;
|
|
268
|
+
class.define_method("_refresh_checkpoint", method!(Admin::refresh_checkpoint, 2))?;
|
|
269
|
+
class.define_method("_delete_checkpoint", method!(Admin::delete_checkpoint, 1))?;
|
|
270
|
+
class.define_method("_run_gc", method!(Admin::run_gc, 1))?;
|
|
271
|
+
|
|
272
|
+
Ok(())
|
|
273
|
+
}
|