lora-ruby 0.5.6 → 0.6.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 +4 -4
- data/README.md +16 -5
- data/lib/lora_ruby/types.rb +17 -1
- data/lib/lora_ruby/version.rb +1 -1
- data/lib/lora_ruby.rb +2 -2
- data/src/lib.rs +328 -26
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3a9450cc524f6ddb0431cab4516dcf68c064a8b6a489e9a920284976b07c624
|
|
4
|
+
data.tar.gz: 132681134206f3fc223cb2238079f2bbc691b201eaeca4771950fdc59788f72d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e08270ea4b1946e2d86bd44ffc40860c12e3c8b1814a8aff02762934c0b0a836a2a4acfc01b2953082f07b863ff21836eb9f2ffb20872c3a8dcf972afd25c3c2
|
|
7
|
+
data.tar.gz: 604298f36756dd70d2ccce2072396db1b508b39a2c1413f2d3a660648be4131e011934314c9d1f260baa7a88be5b012f34feb2cbf318a2261176cde0eb19d39b
|
data/README.md
CHANGED
|
@@ -76,8 +76,9 @@ binding's `lora_python.Database` and because it mirrors the
|
|
|
76
76
|
## Public API
|
|
77
77
|
|
|
78
78
|
```ruby
|
|
79
|
-
LoraRuby::Database.create(
|
|
80
|
-
LoraRuby::Database.new(
|
|
79
|
+
LoraRuby::Database.create(database_name = nil, options = nil) # -> Database
|
|
80
|
+
LoraRuby::Database.new(database_name = nil, options = nil) # -> Database
|
|
81
|
+
LoraRuby::Database.open_wal(wal_dir, options = nil) # -> Database
|
|
81
82
|
|
|
82
83
|
db.execute(query, params = nil) # -> { "columns" => [...], "rows" => [...] }
|
|
83
84
|
db.clear # -> nil
|
|
@@ -156,9 +157,19 @@ replays committed writes before returning the handle.
|
|
|
156
157
|
Call `db.close` before reopening the same archive inside one
|
|
157
158
|
process.
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
For explicit WAL directories with managed snapshots, use `open_wal`:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
db = LoraRuby::Database.open_wal(
|
|
164
|
+
"./data/wal",
|
|
165
|
+
snapshot_dir: "./data/snapshots",
|
|
166
|
+
snapshot_every_commits: 1000,
|
|
167
|
+
snapshot_keep_old: 2,
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`snapshot_options` accepts the same compression/encryption options as
|
|
172
|
+
`save_snapshot`.
|
|
162
173
|
|
|
163
174
|
## Concurrency (GVL release)
|
|
164
175
|
|
data/lib/lora_ruby/types.rb
CHANGED
|
@@ -7,7 +7,7 @@ module LoraRuby
|
|
|
7
7
|
# - Scalars pass through as Ruby natives (`nil`, `true`, `false`,
|
|
8
8
|
# `Integer`, `Float`, `String`).
|
|
9
9
|
# - Lists and maps come back as `Array` / `Hash` (string keys).
|
|
10
|
-
# - Graph, temporal, and
|
|
10
|
+
# - Graph, temporal, spatial, vector, and binary values come back as plain `Hash`es
|
|
11
11
|
# with a `"kind"` discriminator.
|
|
12
12
|
#
|
|
13
13
|
# If you want to narrow a value explicitly, use the `node?` / `point?`
|
|
@@ -56,6 +56,21 @@ module LoraRuby
|
|
|
56
56
|
}
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# Binary / blob constructor — segmented bytes. The native extension
|
|
61
|
+
# accepts each segment as a Ruby String and preserves segment
|
|
62
|
+
# boundaries in WAL/snapshot storage.
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def binary(segments)
|
|
66
|
+
copied = segments.map { |segment| segment.b.dup }
|
|
67
|
+
{
|
|
68
|
+
"kind" => "binary",
|
|
69
|
+
"length" => copied.sum(&:bytesize),
|
|
70
|
+
"segments" => copied,
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
59
74
|
# ------------------------------------------------------------------
|
|
60
75
|
# Spatial constructors — mirrors lora_python.cartesian / wgs84.
|
|
61
76
|
# `cartesian(1, 2)` returns a 2D cartesian point; use `cartesian_3d`
|
|
@@ -122,6 +137,7 @@ module LoraRuby
|
|
|
122
137
|
def path?(v) = tagged?(v, "path")
|
|
123
138
|
def point?(v) = tagged?(v, "point")
|
|
124
139
|
def vector?(v) = tagged?(v, "vector")
|
|
140
|
+
def binary?(v) = tagged?(v, "binary")
|
|
125
141
|
|
|
126
142
|
def temporal?(v)
|
|
127
143
|
return false unless v.is_a?(Hash)
|
data/lib/lora_ruby/version.rb
CHANGED
|
@@ -10,5 +10,5 @@ module LoraRuby
|
|
|
10
10
|
# Guard against redefinition so re-requiring this file (or loading
|
|
11
11
|
# both paths) doesn't emit a "warning: already initialized constant"
|
|
12
12
|
# when the native extension loads second with the identical value.
|
|
13
|
-
VERSION = "0.
|
|
13
|
+
VERSION = "0.6.0" unless const_defined?(:VERSION)
|
|
14
14
|
end
|
data/lib/lora_ruby.rb
CHANGED
|
@@ -32,8 +32,8 @@ module LoraRuby
|
|
|
32
32
|
%i[
|
|
33
33
|
date time localtime datetime localdatetime duration
|
|
34
34
|
cartesian cartesian_3d wgs84 wgs84_3d
|
|
35
|
-
vector
|
|
36
|
-
node? relationship? path? point? temporal? vector?
|
|
35
|
+
vector binary
|
|
36
|
+
node? relationship? path? point? temporal? vector? binary?
|
|
37
37
|
].each do |m|
|
|
38
38
|
define_singleton_method(m) do |*args, **kwargs|
|
|
39
39
|
if kwargs.empty?
|
data/src/lib.rs
CHANGED
|
@@ -25,11 +25,11 @@ use magnus::{
|
|
|
25
25
|
|
|
26
26
|
use lora_database::{
|
|
27
27
|
Database as InnerDatabase, DatabaseOpenOptions, ExecuteOptions, InMemoryGraph, LoraValue,
|
|
28
|
-
QueryResult, ResultFormat,
|
|
28
|
+
QueryResult, ResultFormat, SnapshotConfig, SnapshotOptions, WalConfig,
|
|
29
29
|
};
|
|
30
30
|
use lora_store::{
|
|
31
|
-
LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint,
|
|
32
|
-
LoraVector, RawCoordinate, VectorCoordinateType, VectorValues,
|
|
31
|
+
LoraBinary, LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint,
|
|
32
|
+
LoraTime, LoraVector, RawCoordinate, VectorCoordinateType, VectorValues,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// ============================================================================
|
|
@@ -63,6 +63,7 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
63
63
|
let database = lora_ruby.define_class("Database", ruby.class_object())?;
|
|
64
64
|
database.define_singleton_method("create", function!(database_create, -1))?;
|
|
65
65
|
database.define_singleton_method("new", function!(database_new, -1))?;
|
|
66
|
+
database.define_singleton_method("open_wal", function!(database_open_wal, -1))?;
|
|
66
67
|
database.define_method("execute", method!(database_execute, -1))?;
|
|
67
68
|
database.define_method("clear", method!(database_clear, 0))?;
|
|
68
69
|
database.define_method("close", method!(database_close, 0))?;
|
|
@@ -73,8 +74,8 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
73
74
|
)?;
|
|
74
75
|
database.define_method("inspect", method!(database_inspect, 0))?;
|
|
75
76
|
database.define_method("to_s", method!(database_inspect, 0))?;
|
|
76
|
-
database.define_method("save_snapshot", method!(database_save_snapshot, 1))?;
|
|
77
|
-
database.define_method("load_snapshot", method!(database_load_snapshot, 1))?;
|
|
77
|
+
database.define_method("save_snapshot", method!(database_save_snapshot, -1))?;
|
|
78
|
+
database.define_method("load_snapshot", method!(database_load_snapshot, -1))?;
|
|
78
79
|
|
|
79
80
|
// `LoraRuby::VERSION` is owned by `lib/lora_ruby/version.rb` so the
|
|
80
81
|
// gem can expose a version before the native extension compiles
|
|
@@ -126,6 +127,34 @@ struct Database {
|
|
|
126
127
|
db: Mutex<Option<Arc<InnerDatabase<InMemoryGraph>>>>,
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
#[derive(Default)]
|
|
131
|
+
struct RubyDatabaseOpenOptions {
|
|
132
|
+
named: DatabaseOpenOptions,
|
|
133
|
+
has_database_dir: bool,
|
|
134
|
+
wal_dir: Option<String>,
|
|
135
|
+
snapshot_dir: Option<String>,
|
|
136
|
+
snapshot_every_commits: Option<u64>,
|
|
137
|
+
snapshot_keep_old: Option<usize>,
|
|
138
|
+
has_snapshot_codec: bool,
|
|
139
|
+
snapshot_codec: SnapshotOptions,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl RubyDatabaseOpenOptions {
|
|
143
|
+
fn has_explicit_wal_options(&self) -> bool {
|
|
144
|
+
self.wal_dir.is_some()
|
|
145
|
+
|| self.snapshot_dir.is_some()
|
|
146
|
+
|| self.snapshot_every_commits.is_some()
|
|
147
|
+
|| self.snapshot_keep_old.is_some()
|
|
148
|
+
|| self.has_snapshot_codec
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn has_snapshot_tuning_options(&self) -> bool {
|
|
152
|
+
self.snapshot_every_commits.is_some()
|
|
153
|
+
|| self.snapshot_keep_old.is_some()
|
|
154
|
+
|| self.has_snapshot_codec
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
129
158
|
impl Database {
|
|
130
159
|
fn from_db(db: Arc<InnerDatabase<InMemoryGraph>>) -> Self {
|
|
131
160
|
Self {
|
|
@@ -148,6 +177,35 @@ fn database_create(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError>
|
|
|
148
177
|
database_new(ruby, args)
|
|
149
178
|
}
|
|
150
179
|
|
|
180
|
+
fn database_open_wal(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
181
|
+
let (wal_dir, mut options) = match args.len() {
|
|
182
|
+
1 | 2 => {
|
|
183
|
+
let wal_dir = RString::try_convert(args[0])?.to_string()?;
|
|
184
|
+
let options = if args.len() == 2 {
|
|
185
|
+
ruby_database_open_options(ruby, RHash::try_convert(args[1])?)?
|
|
186
|
+
} else {
|
|
187
|
+
RubyDatabaseOpenOptions::default()
|
|
188
|
+
};
|
|
189
|
+
(wal_dir, options)
|
|
190
|
+
}
|
|
191
|
+
n => {
|
|
192
|
+
return Err(MagnusError::new(
|
|
193
|
+
ruby.exception_arg_error(),
|
|
194
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
if options.wal_dir.is_some() {
|
|
199
|
+
return Err(invalid_params(
|
|
200
|
+
ruby,
|
|
201
|
+
"wal_dir must be passed as the first argument to open_wal",
|
|
202
|
+
));
|
|
203
|
+
}
|
|
204
|
+
options.wal_dir = Some(wal_dir);
|
|
205
|
+
let db = without_gvl(move || open_wal_database(options)).map_err(|e| query_error(ruby, e))?;
|
|
206
|
+
Ok(Database::from_db(db))
|
|
207
|
+
}
|
|
208
|
+
|
|
151
209
|
fn database_clear(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
152
210
|
database_inner(ruby, rb_self)?.clear();
|
|
153
211
|
Ok(())
|
|
@@ -189,11 +247,11 @@ fn database_inspect(rb_self: &Database) -> String {
|
|
|
189
247
|
fn database_save_snapshot(
|
|
190
248
|
ruby: &Ruby,
|
|
191
249
|
rb_self: &Database,
|
|
192
|
-
|
|
250
|
+
args: &[Value],
|
|
193
251
|
) -> Result<RHash, MagnusError> {
|
|
194
|
-
let path =
|
|
252
|
+
let (path, options) = snapshot_file_args(ruby, args)?;
|
|
195
253
|
let db = database_inner(ruby, rb_self)?;
|
|
196
|
-
let meta = without_gvl(move || db.
|
|
254
|
+
let meta = without_gvl(move || db.save_snapshot_to_with_options(&path, &options))
|
|
197
255
|
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
198
256
|
snapshot_meta_to_rhash(ruby, meta)
|
|
199
257
|
}
|
|
@@ -201,15 +259,62 @@ fn database_save_snapshot(
|
|
|
201
259
|
fn database_load_snapshot(
|
|
202
260
|
ruby: &Ruby,
|
|
203
261
|
rb_self: &Database,
|
|
204
|
-
|
|
262
|
+
args: &[Value],
|
|
205
263
|
) -> Result<RHash, MagnusError> {
|
|
206
|
-
let path =
|
|
264
|
+
let (path, credentials) = snapshot_load_file_args(ruby, args)?;
|
|
207
265
|
let db = database_inner(ruby, rb_self)?;
|
|
208
|
-
let meta =
|
|
209
|
-
|
|
266
|
+
let meta =
|
|
267
|
+
without_gvl(move || db.load_snapshot_from_with_credentials(&path, credentials.as_ref()))
|
|
268
|
+
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
210
269
|
snapshot_meta_to_rhash(ruby, meta)
|
|
211
270
|
}
|
|
212
271
|
|
|
272
|
+
fn snapshot_file_args(
|
|
273
|
+
ruby: &Ruby,
|
|
274
|
+
args: &[Value],
|
|
275
|
+
) -> Result<(String, lora_database::SnapshotOptions), MagnusError> {
|
|
276
|
+
match args.len() {
|
|
277
|
+
1 | 2 => {
|
|
278
|
+
let path = RString::try_convert(args[0])?.to_string()?;
|
|
279
|
+
let json = if args.len() == 2 {
|
|
280
|
+
ruby_optional_to_json(ruby, args[1])?
|
|
281
|
+
} else {
|
|
282
|
+
None
|
|
283
|
+
};
|
|
284
|
+
let options = lora_database::snapshot_options_from_json(json)
|
|
285
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot options: {e}")))?;
|
|
286
|
+
Ok((path, options))
|
|
287
|
+
}
|
|
288
|
+
n => Err(MagnusError::new(
|
|
289
|
+
ruby.exception_arg_error(),
|
|
290
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
291
|
+
)),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn snapshot_load_file_args(
|
|
296
|
+
ruby: &Ruby,
|
|
297
|
+
args: &[Value],
|
|
298
|
+
) -> Result<(String, Option<lora_database::SnapshotCredentials>), MagnusError> {
|
|
299
|
+
match args.len() {
|
|
300
|
+
1 | 2 => {
|
|
301
|
+
let path = RString::try_convert(args[0])?.to_string()?;
|
|
302
|
+
let json = if args.len() == 2 {
|
|
303
|
+
ruby_optional_to_json(ruby, args[1])?
|
|
304
|
+
} else {
|
|
305
|
+
None
|
|
306
|
+
};
|
|
307
|
+
let credentials = lora_database::snapshot_credentials_from_json(json)
|
|
308
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot credentials: {e}")))?;
|
|
309
|
+
Ok((path, credentials))
|
|
310
|
+
}
|
|
311
|
+
n => Err(MagnusError::new(
|
|
312
|
+
ruby.exception_arg_error(),
|
|
313
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
314
|
+
)),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
213
318
|
fn snapshot_meta_to_rhash(
|
|
214
319
|
ruby: &Ruby,
|
|
215
320
|
meta: lora_database::SnapshotMeta,
|
|
@@ -237,16 +342,18 @@ fn snapshot_meta_to_rhash(
|
|
|
237
342
|
fn database_open_args(
|
|
238
343
|
ruby: &Ruby,
|
|
239
344
|
args: &[Value],
|
|
240
|
-
) -> Result<(Option<String>,
|
|
345
|
+
) -> Result<(Option<String>, RubyDatabaseOpenOptions), MagnusError> {
|
|
241
346
|
match args.len() {
|
|
242
|
-
0 => Ok((None,
|
|
347
|
+
0 => Ok((None, RubyDatabaseOpenOptions::default())),
|
|
243
348
|
1 => {
|
|
244
349
|
if args[0].is_nil() {
|
|
245
|
-
Ok((None,
|
|
350
|
+
Ok((None, RubyDatabaseOpenOptions::default()))
|
|
351
|
+
} else if let Ok(hash) = RHash::try_convert(args[0]) {
|
|
352
|
+
Ok((None, ruby_database_open_options(ruby, hash)?))
|
|
246
353
|
} else {
|
|
247
354
|
Ok((
|
|
248
355
|
Some(RString::try_convert(args[0])?.to_string()?),
|
|
249
|
-
|
|
356
|
+
RubyDatabaseOpenOptions::default(),
|
|
250
357
|
))
|
|
251
358
|
}
|
|
252
359
|
}
|
|
@@ -256,13 +363,7 @@ fn database_open_args(
|
|
|
256
363
|
} else {
|
|
257
364
|
Some(RString::try_convert(args[0])?.to_string()?)
|
|
258
365
|
};
|
|
259
|
-
let
|
|
260
|
-
let hash = RHash::try_convert(args[1])?;
|
|
261
|
-
if let Some(dir) = hash_get_either(ruby, hash, "database_dir")
|
|
262
|
-
.or_else(|| hash_get_either(ruby, hash, "databaseDir"))
|
|
263
|
-
{
|
|
264
|
-
options.database_dir = RString::try_convert(dir)?.to_string()?.into();
|
|
265
|
-
}
|
|
366
|
+
let options = ruby_database_open_options(ruby, RHash::try_convert(args[1])?)?;
|
|
266
367
|
Ok((database_name, options))
|
|
267
368
|
}
|
|
268
369
|
n => Err(MagnusError::new(
|
|
@@ -272,13 +373,94 @@ fn database_open_args(
|
|
|
272
373
|
}
|
|
273
374
|
}
|
|
274
375
|
|
|
376
|
+
fn ruby_database_open_options(
|
|
377
|
+
ruby: &Ruby,
|
|
378
|
+
hash: RHash,
|
|
379
|
+
) -> Result<RubyDatabaseOpenOptions, MagnusError> {
|
|
380
|
+
let mut options = RubyDatabaseOpenOptions::default();
|
|
381
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["database_dir", "databaseDir"]) {
|
|
382
|
+
options.named.database_dir = RString::try_convert(dir)?.to_string()?.into();
|
|
383
|
+
options.has_database_dir = true;
|
|
384
|
+
}
|
|
385
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["wal_dir", "walDir"]) {
|
|
386
|
+
options.wal_dir = Some(RString::try_convert(dir)?.to_string()?);
|
|
387
|
+
}
|
|
388
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["snapshot_dir", "snapshotDir"]) {
|
|
389
|
+
options.snapshot_dir = Some(RString::try_convert(dir)?.to_string()?);
|
|
390
|
+
}
|
|
391
|
+
if let Some(value) = hash_get_any(
|
|
392
|
+
ruby,
|
|
393
|
+
hash,
|
|
394
|
+
&["snapshot_every_commits", "snapshotEveryCommits"],
|
|
395
|
+
) {
|
|
396
|
+
options.snapshot_every_commits = Some(read_nonnegative_u64(ruby, value)?);
|
|
397
|
+
}
|
|
398
|
+
if let Some(value) = hash_get_any(ruby, hash, &["snapshot_keep_old", "snapshotKeepOld"]) {
|
|
399
|
+
let keep_old = read_nonnegative_u64(ruby, value)?;
|
|
400
|
+
options.snapshot_keep_old = Some(
|
|
401
|
+
usize::try_from(keep_old)
|
|
402
|
+
.map_err(|_| invalid_params(ruby, "snapshot_keep_old does not fit in usize"))?,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if let Some(value) = hash_get_any(ruby, hash, &["snapshot_options", "snapshotOptions"]) {
|
|
406
|
+
let json = ruby_optional_to_json(ruby, value)?;
|
|
407
|
+
options.has_snapshot_codec = true;
|
|
408
|
+
options.snapshot_codec = lora_database::snapshot_options_from_json(json)
|
|
409
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot options: {e}")))?;
|
|
410
|
+
}
|
|
411
|
+
Ok(options)
|
|
412
|
+
}
|
|
413
|
+
|
|
275
414
|
fn open_database(
|
|
276
415
|
database_name: Option<String>,
|
|
277
|
-
options:
|
|
416
|
+
options: RubyDatabaseOpenOptions,
|
|
278
417
|
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
418
|
+
if options.has_explicit_wal_options() {
|
|
419
|
+
return Err(
|
|
420
|
+
"wal_dir/snapshot_dir are not valid for Database.create; use Database.open_wal"
|
|
421
|
+
.to_string(),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
279
424
|
let db = match database_name {
|
|
280
|
-
Some(name) => InnerDatabase::open_named(name, options).map_err(|e| e.to_string())?,
|
|
281
|
-
None =>
|
|
425
|
+
Some(name) => InnerDatabase::open_named(name, options.named).map_err(|e| e.to_string())?,
|
|
426
|
+
None => {
|
|
427
|
+
if options.has_database_dir {
|
|
428
|
+
return Err("database_name is required when database_dir is provided".to_string());
|
|
429
|
+
}
|
|
430
|
+
InnerDatabase::in_memory()
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
Ok(Arc::new(db))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fn open_wal_database(
|
|
437
|
+
options: RubyDatabaseOpenOptions,
|
|
438
|
+
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
439
|
+
if options.has_database_dir {
|
|
440
|
+
return Err("database_dir is not valid for Database.open_wal".to_string());
|
|
441
|
+
}
|
|
442
|
+
let has_snapshot_tuning = options.has_snapshot_tuning_options();
|
|
443
|
+
if options.snapshot_dir.is_none() && has_snapshot_tuning {
|
|
444
|
+
return Err(
|
|
445
|
+
"snapshot_dir is required when managed snapshot options are provided".to_string(),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
let wal_dir = options
|
|
449
|
+
.wal_dir
|
|
450
|
+
.ok_or_else(|| "wal_dir is required for Database.open_wal".to_string())?;
|
|
451
|
+
let wal_config = WalConfig::enabled(wal_dir);
|
|
452
|
+
let db = if let Some(snapshot_dir) = options.snapshot_dir {
|
|
453
|
+
let mut snapshots = SnapshotConfig::enabled(snapshot_dir)
|
|
454
|
+
.keep_old(options.snapshot_keep_old.unwrap_or(1))
|
|
455
|
+
.codec(options.snapshot_codec);
|
|
456
|
+
if let Some(every) = options.snapshot_every_commits {
|
|
457
|
+
if every != 0 {
|
|
458
|
+
snapshots = snapshots.every_commits(every);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
InnerDatabase::open_with_wal_snapshots(wal_config, snapshots).map_err(|e| e.to_string())?
|
|
462
|
+
} else {
|
|
463
|
+
InnerDatabase::open_with_wal(wal_config).map_err(|e| e.to_string())?
|
|
282
464
|
};
|
|
283
465
|
Ok(Arc::new(db))
|
|
284
466
|
}
|
|
@@ -432,9 +614,25 @@ fn lora_value_to_ruby(ruby: &Ruby, value: &LoraValue) -> Result<Value, MagnusErr
|
|
|
432
614
|
LoraValue::Duration(v) => tagged_iso(ruby, "duration", v.to_string()),
|
|
433
615
|
LoraValue::Point(p) => point_to_ruby(ruby, p),
|
|
434
616
|
LoraValue::Vector(v) => vector_to_ruby(ruby, v),
|
|
617
|
+
LoraValue::Binary(v) => binary_to_ruby(ruby, v),
|
|
435
618
|
}
|
|
436
619
|
}
|
|
437
620
|
|
|
621
|
+
fn binary_to_ruby(ruby: &Ruby, value: &LoraBinary) -> Result<Value, MagnusError> {
|
|
622
|
+
let h = ruby.hash_new();
|
|
623
|
+
h.aset(ruby.str_new("kind"), ruby.str_new("binary"))?;
|
|
624
|
+
h.aset(
|
|
625
|
+
ruby.str_new("length"),
|
|
626
|
+
ruby.integer_from_i64(value.len() as i64),
|
|
627
|
+
)?;
|
|
628
|
+
let segments = ruby.ary_new();
|
|
629
|
+
for segment in value.segments() {
|
|
630
|
+
segments.push(ruby.str_from_slice(segment))?;
|
|
631
|
+
}
|
|
632
|
+
h.aset(ruby.str_new("segments"), segments)?;
|
|
633
|
+
Ok(h.as_value())
|
|
634
|
+
}
|
|
635
|
+
|
|
438
636
|
fn vector_to_ruby(ruby: &Ruby, v: &LoraVector) -> Result<Value, MagnusError> {
|
|
439
637
|
let h = ruby.hash_new();
|
|
440
638
|
h.aset(ruby.str_new("kind"), ruby.str_new("vector"))?;
|
|
@@ -573,6 +771,86 @@ fn coerce_key(ruby: &Ruby, v: Value) -> Result<String, MagnusError> {
|
|
|
573
771
|
Err(invalid_params(ruby, "param keys must be String or Symbol"))
|
|
574
772
|
}
|
|
575
773
|
|
|
774
|
+
fn ruby_optional_to_json(
|
|
775
|
+
ruby: &Ruby,
|
|
776
|
+
value: Value,
|
|
777
|
+
) -> Result<Option<serde_json::Value>, MagnusError> {
|
|
778
|
+
if value.is_nil() {
|
|
779
|
+
Ok(None)
|
|
780
|
+
} else {
|
|
781
|
+
ruby_value_to_json(ruby, value).map(Some)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fn ruby_value_to_json(ruby: &Ruby, value: Value) -> Result<serde_json::Value, MagnusError> {
|
|
786
|
+
if value.is_nil() {
|
|
787
|
+
return Ok(serde_json::Value::Null);
|
|
788
|
+
}
|
|
789
|
+
if value.is_kind_of(ruby.class_true_class()) {
|
|
790
|
+
return Ok(serde_json::Value::Bool(true));
|
|
791
|
+
}
|
|
792
|
+
if value.is_kind_of(ruby.class_false_class()) {
|
|
793
|
+
return Ok(serde_json::Value::Bool(false));
|
|
794
|
+
}
|
|
795
|
+
if let Ok(i) = Integer::try_convert(value) {
|
|
796
|
+
let n = i
|
|
797
|
+
.to_i64()
|
|
798
|
+
.map_err(|_| invalid_params(ruby, "snapshot option integer does not fit in i64"))?;
|
|
799
|
+
return Ok(serde_json::Value::Number(n.into()));
|
|
800
|
+
}
|
|
801
|
+
if let Ok(f) = Float::try_convert(value) {
|
|
802
|
+
let Some(number) = serde_json::Number::from_f64(f.to_f64()) else {
|
|
803
|
+
return Err(invalid_params(ruby, "snapshot option float must be finite"));
|
|
804
|
+
};
|
|
805
|
+
return Ok(serde_json::Value::Number(number));
|
|
806
|
+
}
|
|
807
|
+
if let Ok(s) = RString::try_convert(value) {
|
|
808
|
+
return Ok(serde_json::Value::String(s.to_string()?));
|
|
809
|
+
}
|
|
810
|
+
if let Ok(sym) = Symbol::try_convert(value) {
|
|
811
|
+
return Ok(serde_json::Value::String(sym.name()?.into_owned()));
|
|
812
|
+
}
|
|
813
|
+
if let Ok(arr) = RArray::try_convert(value) {
|
|
814
|
+
let mut out = Vec::with_capacity(arr.len());
|
|
815
|
+
for item in arr.into_iter() {
|
|
816
|
+
out.push(ruby_value_to_json(ruby, item)?);
|
|
817
|
+
}
|
|
818
|
+
return Ok(serde_json::Value::Array(out));
|
|
819
|
+
}
|
|
820
|
+
if let Ok(hash) = RHash::try_convert(value) {
|
|
821
|
+
let mut out = serde_json::Map::new();
|
|
822
|
+
let mut error = None;
|
|
823
|
+
hash.foreach(|k: Value, v: Value| {
|
|
824
|
+
let key = match coerce_key(ruby, k) {
|
|
825
|
+
Ok(key) => key,
|
|
826
|
+
Err(e) => {
|
|
827
|
+
error = Some(e);
|
|
828
|
+
return Ok(ForEach::Stop);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
let json = match ruby_value_to_json(ruby, v) {
|
|
832
|
+
Ok(json) => json,
|
|
833
|
+
Err(e) => {
|
|
834
|
+
error = Some(e);
|
|
835
|
+
return Ok(ForEach::Stop);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
out.insert(key, json);
|
|
839
|
+
Ok(ForEach::Continue)
|
|
840
|
+
})?;
|
|
841
|
+
if let Some(error) = error {
|
|
842
|
+
return Err(error);
|
|
843
|
+
}
|
|
844
|
+
return Ok(serde_json::Value::Object(out));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
let class_name = unsafe { value.classname() }.into_owned();
|
|
848
|
+
Err(invalid_params(
|
|
849
|
+
ruby,
|
|
850
|
+
format!("unsupported snapshot option type: {class_name}"),
|
|
851
|
+
))
|
|
852
|
+
}
|
|
853
|
+
|
|
576
854
|
fn ruby_value_to_lora(ruby: &Ruby, v: Value) -> Result<LoraValue, MagnusError> {
|
|
577
855
|
if v.is_nil() {
|
|
578
856
|
return Ok(LoraValue::Null);
|
|
@@ -665,6 +943,7 @@ fn ruby_hash_to_cypher(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusErro
|
|
|
665
943
|
}
|
|
666
944
|
"point" => return build_point(ruby, hash),
|
|
667
945
|
"vector" => return build_vector(ruby, hash),
|
|
946
|
+
"binary" | "blob" => return build_binary(ruby, hash),
|
|
668
947
|
_ => { /* fall through to plain-map handling */ }
|
|
669
948
|
}
|
|
670
949
|
}
|
|
@@ -765,6 +1044,20 @@ fn build_vector(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusError> {
|
|
|
765
1044
|
Ok(LoraValue::Vector(v))
|
|
766
1045
|
}
|
|
767
1046
|
|
|
1047
|
+
fn build_binary(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusError> {
|
|
1048
|
+
let segments_value = hash_get_either(ruby, hash, "segments")
|
|
1049
|
+
.ok_or_else(|| invalid_params(ruby, "binary.segments required"))?;
|
|
1050
|
+
let arr = RArray::try_convert(segments_value)
|
|
1051
|
+
.map_err(|_| invalid_params(ruby, "binary.segments must be an Array"))?;
|
|
1052
|
+
let mut segments = Vec::with_capacity(arr.len());
|
|
1053
|
+
for item in arr.into_iter() {
|
|
1054
|
+
let segment = RString::try_convert(item)
|
|
1055
|
+
.map_err(|_| invalid_params(ruby, "binary.segments entries must be Strings"))?;
|
|
1056
|
+
segments.push(unsafe { segment.as_slice().to_vec() });
|
|
1057
|
+
}
|
|
1058
|
+
Ok(LoraValue::Binary(LoraBinary::from_segments(segments)))
|
|
1059
|
+
}
|
|
1060
|
+
|
|
768
1061
|
fn read_i64(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<i64>, MagnusError> {
|
|
769
1062
|
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
770
1063
|
return Ok(None);
|
|
@@ -783,6 +1076,15 @@ fn hash_get_either(ruby: &Ruby, hash: RHash, key: &str) -> Option<Value> {
|
|
|
783
1076
|
hash.get(ruby.to_symbol(key))
|
|
784
1077
|
}
|
|
785
1078
|
|
|
1079
|
+
fn hash_get_any(ruby: &Ruby, hash: RHash, keys: &[&str]) -> Option<Value> {
|
|
1080
|
+
keys.iter().find_map(|key| hash_get_either(ruby, hash, key))
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
fn read_nonnegative_u64(ruby: &Ruby, value: Value) -> Result<u64, MagnusError> {
|
|
1084
|
+
let n = Integer::try_convert(value)?.to_i64()?;
|
|
1085
|
+
u64::try_from(n).map_err(|_| invalid_params(ruby, "option integer must be non-negative"))
|
|
1086
|
+
}
|
|
1087
|
+
|
|
786
1088
|
fn read_string(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<String>, MagnusError> {
|
|
787
1089
|
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
788
1090
|
return Ok(None);
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lora-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LoraDB, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|