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,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
+ }