lora-ruby 0.3.0 → 0.5.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 +27 -3
- data/lib/lora_ruby/version.rb +1 -1
- data/src/lib.rs +111 -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: 93108d86cc62dc1e96f8f4f486696b9b422a4ccf62119e56c79131df476a8144
|
|
4
|
+
data.tar.gz: a2d547e01de27be476b969c8cecfdaaf940eda38b58ea39a930a16bf57001c98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53a6ef06bc8d0e456d53100fde0ee2aeae43085d4923b65842f70b014360d7ab18025f70f7ac09a101fb7916cfabe0f385fa632cea1b8105440a24011ef657f7
|
|
7
|
+
data.tar.gz: 90f93b470012d15fb981e79db8ae47913396f95de2a7379e3258749ef5bbaab70b9712d480f268759c96a4e0996bbd1bed044b1406e1dac4ef76edf1aba9ad90
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# lora-ruby
|
|
2
2
|
|
|
3
|
-
Ruby bindings for the [Lora](../../README.md)
|
|
3
|
+
Ruby bindings for the [Lora](../../README.md) graph engine.
|
|
4
4
|
Ships a native extension built with [Magnus](https://github.com/matsadler/magnus)
|
|
5
5
|
on top of [`rb-sys`](https://github.com/oxidize-rb/rb-sys) so the Rust
|
|
6
6
|
engine runs in-process — no separate server, no socket hop.
|
|
@@ -39,6 +39,16 @@ result["rows"].each do |row|
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Initialization rule:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
scratch = LoraRuby::Database.create # in-memory
|
|
46
|
+
persistent = LoraRuby::Database.create("./app") # persistent: directory string
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you want persistence, pass a directory string to
|
|
50
|
+
`LoraRuby::Database.create(...)` or `LoraRuby::Database.new(...)`.
|
|
51
|
+
|
|
42
52
|
### Params
|
|
43
53
|
|
|
44
54
|
`execute` accepts a second argument — either `nil` or a `Hash` keyed by
|
|
@@ -66,8 +76,8 @@ binding's `lora_python.Database` and because it mirrors the
|
|
|
66
76
|
## Public API
|
|
67
77
|
|
|
68
78
|
```ruby
|
|
69
|
-
LoraRuby::Database.create
|
|
70
|
-
LoraRuby::Database.new
|
|
79
|
+
LoraRuby::Database.create(wal_dir = nil) # -> Database
|
|
80
|
+
LoraRuby::Database.new(wal_dir = nil) # -> Database (alias of .create)
|
|
71
81
|
|
|
72
82
|
db.execute(query, params = nil) # -> { "columns" => [...], "rows" => [...] }
|
|
73
83
|
db.clear # -> nil
|
|
@@ -136,6 +146,20 @@ db.execute(
|
|
|
136
146
|
- `LoraRuby::QueryError` — parse / analyze / execute failure.
|
|
137
147
|
- `LoraRuby::InvalidParamsError` — a parameter value couldn't be mapped.
|
|
138
148
|
|
|
149
|
+
## Persistence
|
|
150
|
+
|
|
151
|
+
`LoraRuby::Database.create("./app")` and
|
|
152
|
+
`LoraRuby::Database.new("./app")` open or create a WAL-backed
|
|
153
|
+
persistent database rooted at that directory. Reopening the same path
|
|
154
|
+
replays committed writes before returning the handle.
|
|
155
|
+
|
|
156
|
+
Call `db.close` before reopening the same WAL directory inside one
|
|
157
|
+
process.
|
|
158
|
+
|
|
159
|
+
This first Ruby persistence slice intentionally stays small: the
|
|
160
|
+
binding exposes WAL-backed initialization plus the existing snapshot
|
|
161
|
+
APIs, but not checkpoint, truncate, status, or sync-mode controls.
|
|
162
|
+
|
|
139
163
|
## Concurrency (GVL release)
|
|
140
164
|
|
|
141
165
|
`Database#execute` calls `rb_thread_call_without_gvl`, so other Ruby
|
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.5.0" unless const_defined?(:VERSION)
|
|
14
14
|
end
|
data/src/lib.rs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
use std::collections::BTreeMap;
|
|
17
17
|
use std::ffi::c_void;
|
|
18
18
|
use std::mem::MaybeUninit;
|
|
19
|
-
use std::sync::Arc;
|
|
19
|
+
use std::sync::{Arc, Mutex};
|
|
20
20
|
|
|
21
21
|
use magnus::{
|
|
22
22
|
function, method, prelude::*, r_hash::ForEach, value::ReprValue, Error as MagnusError,
|
|
@@ -24,7 +24,8 @@ use magnus::{
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
use lora_database::{
|
|
27
|
-
Database as InnerDatabase, ExecuteOptions, InMemoryGraph, LoraValue,
|
|
27
|
+
Database as InnerDatabase, DatabaseOpenOptions, ExecuteOptions, InMemoryGraph, LoraValue,
|
|
28
|
+
QueryResult, ResultFormat,
|
|
28
29
|
};
|
|
29
30
|
use lora_store::{
|
|
30
31
|
LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint, LoraTime,
|
|
@@ -60,10 +61,11 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
60
61
|
lora_ruby.define_class("InvalidParamsError", error)?;
|
|
61
62
|
|
|
62
63
|
let database = lora_ruby.define_class("Database", ruby.class_object())?;
|
|
63
|
-
database.define_singleton_method("create", function!(database_create,
|
|
64
|
-
database.define_singleton_method("new", function!(database_new,
|
|
64
|
+
database.define_singleton_method("create", function!(database_create, -1))?;
|
|
65
|
+
database.define_singleton_method("new", function!(database_new, -1))?;
|
|
65
66
|
database.define_method("execute", method!(database_execute, -1))?;
|
|
66
67
|
database.define_method("clear", method!(database_clear, 0))?;
|
|
68
|
+
database.define_method("close", method!(database_close, 0))?;
|
|
67
69
|
database.define_method("node_count", method!(database_node_count, 0))?;
|
|
68
70
|
database.define_method(
|
|
69
71
|
"relationship_count",
|
|
@@ -114,20 +116,20 @@ fn invalid_params(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
114
116
|
// Database
|
|
115
117
|
// ============================================================================
|
|
116
118
|
|
|
117
|
-
///
|
|
119
|
+
/// Lora graph database handle exposed to Ruby.
|
|
118
120
|
///
|
|
119
121
|
/// Wraps an `Arc<Database<InMemoryGraph>>`; the same handle is cloned
|
|
120
122
|
/// across the GVL-release boundary for query execution without borrowing
|
|
121
123
|
/// any Ruby state.
|
|
122
124
|
#[magnus::wrap(class = "LoraRuby::Database", free_immediately, size)]
|
|
123
125
|
struct Database {
|
|
124
|
-
db: Arc<InnerDatabase<InMemoryGraph
|
|
126
|
+
db: Mutex<Option<Arc<InnerDatabase<InMemoryGraph>>>>,
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
impl Database {
|
|
128
|
-
fn
|
|
130
|
+
fn from_db(db: Arc<InnerDatabase<InMemoryGraph>>) -> Self {
|
|
129
131
|
Self {
|
|
130
|
-
db:
|
|
132
|
+
db: Mutex::new(Some(db)),
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
}
|
|
@@ -135,32 +137,53 @@ impl Database {
|
|
|
135
137
|
// Constructors — we expose `Database.create` and `Database.new` as
|
|
136
138
|
// singletons so callers can use whichever idiom they prefer; both are
|
|
137
139
|
// cost-equivalent.
|
|
138
|
-
fn database_new() -> Database {
|
|
139
|
-
|
|
140
|
+
fn database_new(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
141
|
+
let (database_name, options) = database_open_args(ruby, args)?;
|
|
142
|
+
let db = without_gvl(move || open_database(database_name, options))
|
|
143
|
+
.map_err(|e| query_error(ruby, e))?;
|
|
144
|
+
Ok(Database::from_db(db))
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
fn database_create() -> Database {
|
|
143
|
-
|
|
147
|
+
fn database_create(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
148
|
+
database_new(ruby, args)
|
|
144
149
|
}
|
|
145
150
|
|
|
146
|
-
fn database_clear(rb_self: &Database) {
|
|
147
|
-
rb_self
|
|
151
|
+
fn database_clear(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
152
|
+
database_inner(ruby, rb_self)?.clear();
|
|
153
|
+
Ok(())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn database_close(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
157
|
+
let mut slot = rb_self
|
|
158
|
+
.db
|
|
159
|
+
.lock()
|
|
160
|
+
.map_err(|_| query_error(ruby, "database lock poisoned"))?;
|
|
161
|
+
slot.take();
|
|
162
|
+
Ok(())
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
fn database_node_count(rb_self: &Database) -> u64 {
|
|
151
|
-
rb_self
|
|
165
|
+
fn database_node_count(ruby: &Ruby, rb_self: &Database) -> Result<u64, MagnusError> {
|
|
166
|
+
Ok(database_inner(ruby, rb_self)?.node_count() as u64)
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
fn database_relationship_count(rb_self: &Database) -> u64 {
|
|
155
|
-
rb_self
|
|
169
|
+
fn database_relationship_count(ruby: &Ruby, rb_self: &Database) -> Result<u64, MagnusError> {
|
|
170
|
+
Ok(database_inner(ruby, rb_self)?.relationship_count() as u64)
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
fn database_inspect(rb_self: &Database) -> String {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
match rb_self
|
|
175
|
+
.db
|
|
176
|
+
.lock()
|
|
177
|
+
.ok()
|
|
178
|
+
.and_then(|slot| slot.as_ref().cloned())
|
|
179
|
+
{
|
|
180
|
+
Some(db) => format!(
|
|
181
|
+
"#<LoraRuby::Database nodes={} relationships={}>",
|
|
182
|
+
db.node_count(),
|
|
183
|
+
db.relationship_count(),
|
|
184
|
+
),
|
|
185
|
+
None => "#<LoraRuby::Database closed>".to_string(),
|
|
186
|
+
}
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
fn database_save_snapshot(
|
|
@@ -169,7 +192,7 @@ fn database_save_snapshot(
|
|
|
169
192
|
path: RString,
|
|
170
193
|
) -> Result<RHash, MagnusError> {
|
|
171
194
|
let path = path.to_string()?;
|
|
172
|
-
let db =
|
|
195
|
+
let db = database_inner(ruby, rb_self)?;
|
|
173
196
|
let meta = without_gvl(move || db.save_snapshot_to(&path))
|
|
174
197
|
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
175
198
|
snapshot_meta_to_rhash(ruby, meta)
|
|
@@ -181,7 +204,7 @@ fn database_load_snapshot(
|
|
|
181
204
|
path: RString,
|
|
182
205
|
) -> Result<RHash, MagnusError> {
|
|
183
206
|
let path = path.to_string()?;
|
|
184
|
-
let db =
|
|
207
|
+
let db = database_inner(ruby, rb_self)?;
|
|
185
208
|
let meta = without_gvl(move || db.load_snapshot_from(&path))
|
|
186
209
|
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
187
210
|
snapshot_meta_to_rhash(ruby, meta)
|
|
@@ -211,6 +234,68 @@ fn snapshot_meta_to_rhash(
|
|
|
211
234
|
Ok(h)
|
|
212
235
|
}
|
|
213
236
|
|
|
237
|
+
fn database_open_args(
|
|
238
|
+
ruby: &Ruby,
|
|
239
|
+
args: &[Value],
|
|
240
|
+
) -> Result<(Option<String>, DatabaseOpenOptions), MagnusError> {
|
|
241
|
+
match args.len() {
|
|
242
|
+
0 => Ok((None, DatabaseOpenOptions::default())),
|
|
243
|
+
1 => {
|
|
244
|
+
if args[0].is_nil() {
|
|
245
|
+
Ok((None, DatabaseOpenOptions::default()))
|
|
246
|
+
} else {
|
|
247
|
+
Ok((
|
|
248
|
+
Some(RString::try_convert(args[0])?.to_string()?),
|
|
249
|
+
DatabaseOpenOptions::default(),
|
|
250
|
+
))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
2 => {
|
|
254
|
+
let database_name = if args[0].is_nil() {
|
|
255
|
+
None
|
|
256
|
+
} else {
|
|
257
|
+
Some(RString::try_convert(args[0])?.to_string()?)
|
|
258
|
+
};
|
|
259
|
+
let mut options = DatabaseOpenOptions::default();
|
|
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
|
+
}
|
|
266
|
+
Ok((database_name, options))
|
|
267
|
+
}
|
|
268
|
+
n => Err(MagnusError::new(
|
|
269
|
+
ruby.exception_arg_error(),
|
|
270
|
+
format!("wrong number of arguments (given {n}, expected 0..2)"),
|
|
271
|
+
)),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn open_database(
|
|
276
|
+
database_name: Option<String>,
|
|
277
|
+
options: DatabaseOpenOptions,
|
|
278
|
+
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
279
|
+
let db = match database_name {
|
|
280
|
+
Some(name) => InnerDatabase::open_named(name, options).map_err(|e| e.to_string())?,
|
|
281
|
+
None => InnerDatabase::in_memory(),
|
|
282
|
+
};
|
|
283
|
+
Ok(Arc::new(db))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn database_inner(
|
|
287
|
+
ruby: &Ruby,
|
|
288
|
+
rb_self: &Database,
|
|
289
|
+
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, MagnusError> {
|
|
290
|
+
let slot = rb_self
|
|
291
|
+
.db
|
|
292
|
+
.lock()
|
|
293
|
+
.map_err(|_| query_error(ruby, "database lock poisoned"))?;
|
|
294
|
+
slot.as_ref()
|
|
295
|
+
.cloned()
|
|
296
|
+
.ok_or_else(|| query_error(ruby, "database is closed"))
|
|
297
|
+
}
|
|
298
|
+
|
|
214
299
|
/// `execute(query, params = nil)` — `-1` arity so `params` is optional and
|
|
215
300
|
/// we can distinguish "not passed" from `nil`/`{}` (both map to empty
|
|
216
301
|
/// params). Everything that touches Ruby values happens under the GVL;
|
|
@@ -248,7 +333,7 @@ fn database_execute(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
|
|
|
248
333
|
// Run the engine with the GVL released. Everything inside the closure
|
|
249
334
|
// is pure Rust — no Ruby values cross the boundary — which keeps this
|
|
250
335
|
// sound.
|
|
251
|
-
let db =
|
|
336
|
+
let db = database_inner(ruby, rb_self)?;
|
|
252
337
|
let exec_result = without_gvl(move || {
|
|
253
338
|
let options = ExecuteOptions {
|
|
254
339
|
format: ResultFormat::RowArrays,
|
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.5.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-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|