lora-ruby 0.2.0 → 0.4.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 +139 -32
- 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: 9f225172245d7983494a2d5d498edc749e31629b811453667be8517d7624fde0
|
|
4
|
+
data.tar.gz: 36e8d24e24003755b36abcf34354df4c7af42125d969c27c2e76de551d2cb06d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48356ff0802ae5949f2d000580779c865b793dc562d8068deb92e72eefc7a2409fa9890b7a69ff336ef8c0ba2d67c27da57f253e6db2674920715b71320f7c07
|
|
7
|
+
data.tar.gz: 4446b8aac9ac26302ccb688784c6bd20f3e6f4ca80d6fb66525505c7022964f672cf9272876584672ce8b34a183708a50f9cc54e785ae75420394b365ecc43f2
|
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.4.0" unless const_defined?(:VERSION)
|
|
14
14
|
end
|
data/src/lib.rs
CHANGED
|
@@ -25,10 +25,11 @@ use magnus::{
|
|
|
25
25
|
|
|
26
26
|
use lora_database::{
|
|
27
27
|
Database as InnerDatabase, ExecuteOptions, InMemoryGraph, LoraValue, QueryResult, ResultFormat,
|
|
28
|
+
WalConfig,
|
|
28
29
|
};
|
|
29
30
|
use lora_store::{
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint, LoraTime,
|
|
32
|
+
LoraVector, RawCoordinate, VectorCoordinateType, VectorValues,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
// ============================================================================
|
|
@@ -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",
|
|
@@ -71,6 +73,8 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
71
73
|
)?;
|
|
72
74
|
database.define_method("inspect", method!(database_inspect, 0))?;
|
|
73
75
|
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))?;
|
|
74
78
|
|
|
75
79
|
// `LoraRuby::VERSION` is owned by `lib/lora_ruby/version.rb` so the
|
|
76
80
|
// gem can expose a version before the native extension compiles
|
|
@@ -112,20 +116,20 @@ fn invalid_params(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
|
112
116
|
// Database
|
|
113
117
|
// ============================================================================
|
|
114
118
|
|
|
115
|
-
///
|
|
119
|
+
/// Lora graph database handle exposed to Ruby.
|
|
116
120
|
///
|
|
117
|
-
/// `Arc<
|
|
118
|
-
///
|
|
119
|
-
/// state.
|
|
121
|
+
/// Wraps an `Arc<Database<InMemoryGraph>>`; the same handle is cloned
|
|
122
|
+
/// across the GVL-release boundary for query execution without borrowing
|
|
123
|
+
/// any Ruby state.
|
|
120
124
|
#[magnus::wrap(class = "LoraRuby::Database", free_immediately, size)]
|
|
121
125
|
struct Database {
|
|
122
|
-
|
|
126
|
+
db: Mutex<Option<Arc<InnerDatabase<InMemoryGraph>>>>,
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
impl Database {
|
|
126
|
-
fn
|
|
130
|
+
fn from_db(db: Arc<InnerDatabase<InMemoryGraph>>) -> Self {
|
|
127
131
|
Self {
|
|
128
|
-
|
|
132
|
+
db: Mutex::new(Some(db)),
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
}
|
|
@@ -133,36 +137,140 @@ impl Database {
|
|
|
133
137
|
// Constructors — we expose `Database.create` and `Database.new` as
|
|
134
138
|
// singletons so callers can use whichever idiom they prefer; both are
|
|
135
139
|
// cost-equivalent.
|
|
136
|
-
fn database_new() -> Database {
|
|
137
|
-
|
|
140
|
+
fn database_new(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
141
|
+
let wal_dir = optional_wal_dir_arg(ruby, args)?;
|
|
142
|
+
let db = without_gvl(move || open_database(wal_dir)).map_err(|e| query_error(ruby, e))?;
|
|
143
|
+
Ok(Database::from_db(db))
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
fn database_create() -> Database {
|
|
141
|
-
|
|
146
|
+
fn database_create(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
147
|
+
database_new(ruby, args)
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
fn database_clear(rb_self: &Database) {
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
fn database_clear(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
151
|
+
database_inner(ruby, rb_self)?.clear();
|
|
152
|
+
Ok(())
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
fn
|
|
150
|
-
let
|
|
151
|
-
|
|
155
|
+
fn database_close(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
156
|
+
let mut slot = rb_self
|
|
157
|
+
.db
|
|
158
|
+
.lock()
|
|
159
|
+
.map_err(|_| query_error(ruby, "database lock poisoned"))?;
|
|
160
|
+
slot.take();
|
|
161
|
+
Ok(())
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
fn
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
fn database_node_count(ruby: &Ruby, rb_self: &Database) -> Result<u64, MagnusError> {
|
|
165
|
+
Ok(database_inner(ruby, rb_self)?.node_count() as u64)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fn database_relationship_count(ruby: &Ruby, rb_self: &Database) -> Result<u64, MagnusError> {
|
|
169
|
+
Ok(database_inner(ruby, rb_self)?.relationship_count() as u64)
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
fn database_inspect(rb_self: &Database) -> String {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
match rb_self
|
|
174
|
+
.db
|
|
175
|
+
.lock()
|
|
176
|
+
.ok()
|
|
177
|
+
.and_then(|slot| slot.as_ref().cloned())
|
|
178
|
+
{
|
|
179
|
+
Some(db) => format!(
|
|
180
|
+
"#<LoraRuby::Database nodes={} relationships={}>",
|
|
181
|
+
db.node_count(),
|
|
182
|
+
db.relationship_count(),
|
|
183
|
+
),
|
|
184
|
+
None => "#<LoraRuby::Database closed>".to_string(),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn database_save_snapshot(
|
|
189
|
+
ruby: &Ruby,
|
|
190
|
+
rb_self: &Database,
|
|
191
|
+
path: RString,
|
|
192
|
+
) -> Result<RHash, MagnusError> {
|
|
193
|
+
let path = path.to_string()?;
|
|
194
|
+
let db = database_inner(ruby, rb_self)?;
|
|
195
|
+
let meta = without_gvl(move || db.save_snapshot_to(&path))
|
|
196
|
+
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
197
|
+
snapshot_meta_to_rhash(ruby, meta)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn database_load_snapshot(
|
|
201
|
+
ruby: &Ruby,
|
|
202
|
+
rb_self: &Database,
|
|
203
|
+
path: RString,
|
|
204
|
+
) -> Result<RHash, MagnusError> {
|
|
205
|
+
let path = path.to_string()?;
|
|
206
|
+
let db = database_inner(ruby, rb_self)?;
|
|
207
|
+
let meta = without_gvl(move || db.load_snapshot_from(&path))
|
|
208
|
+
.map_err(|e| query_error(ruby, format!("{e}")))?;
|
|
209
|
+
snapshot_meta_to_rhash(ruby, meta)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn snapshot_meta_to_rhash(
|
|
213
|
+
ruby: &Ruby,
|
|
214
|
+
meta: lora_database::SnapshotMeta,
|
|
215
|
+
) -> Result<RHash, MagnusError> {
|
|
216
|
+
let h = ruby.hash_new();
|
|
217
|
+
h.aset(
|
|
218
|
+
ruby.str_new("formatVersion"),
|
|
219
|
+
ruby.integer_from_i64(meta.format_version as i64),
|
|
220
|
+
)?;
|
|
221
|
+
h.aset(
|
|
222
|
+
ruby.str_new("nodeCount"),
|
|
223
|
+
ruby.integer_from_i64(meta.node_count as i64),
|
|
224
|
+
)?;
|
|
225
|
+
h.aset(
|
|
226
|
+
ruby.str_new("relationshipCount"),
|
|
227
|
+
ruby.integer_from_i64(meta.relationship_count as i64),
|
|
228
|
+
)?;
|
|
229
|
+
match meta.wal_lsn {
|
|
230
|
+
Some(lsn) => h.aset(ruby.str_new("walLsn"), ruby.integer_from_i64(lsn as i64))?,
|
|
231
|
+
None => h.aset(ruby.str_new("walLsn"), ruby.qnil())?,
|
|
232
|
+
}
|
|
233
|
+
Ok(h)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn optional_wal_dir_arg(ruby: &Ruby, args: &[Value]) -> Result<Option<String>, MagnusError> {
|
|
237
|
+
match args.len() {
|
|
238
|
+
0 => Ok(None),
|
|
239
|
+
1 => {
|
|
240
|
+
if args[0].is_nil() {
|
|
241
|
+
Ok(None)
|
|
242
|
+
} else {
|
|
243
|
+
Ok(Some(RString::try_convert(args[0])?.to_string()?))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
n => Err(MagnusError::new(
|
|
247
|
+
ruby.exception_arg_error(),
|
|
248
|
+
format!("wrong number of arguments (given {n}, expected 0..1)"),
|
|
249
|
+
)),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn open_database(wal_dir: Option<String>) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
254
|
+
let db = match wal_dir {
|
|
255
|
+
Some(dir) => {
|
|
256
|
+
InnerDatabase::open_with_wal(WalConfig::enabled(dir)).map_err(|e| e.to_string())?
|
|
257
|
+
}
|
|
258
|
+
None => InnerDatabase::in_memory(),
|
|
259
|
+
};
|
|
260
|
+
Ok(Arc::new(db))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn database_inner(
|
|
264
|
+
ruby: &Ruby,
|
|
265
|
+
rb_self: &Database,
|
|
266
|
+
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, MagnusError> {
|
|
267
|
+
let slot = rb_self
|
|
268
|
+
.db
|
|
269
|
+
.lock()
|
|
270
|
+
.map_err(|_| query_error(ruby, "database lock poisoned"))?;
|
|
271
|
+
slot.as_ref()
|
|
272
|
+
.cloned()
|
|
273
|
+
.ok_or_else(|| query_error(ruby, "database is closed"))
|
|
166
274
|
}
|
|
167
275
|
|
|
168
276
|
/// `execute(query, params = nil)` — `-1` arity so `params` is optional and
|
|
@@ -202,9 +310,8 @@ fn database_execute(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
|
|
|
202
310
|
// Run the engine with the GVL released. Everything inside the closure
|
|
203
311
|
// is pure Rust — no Ruby values cross the boundary — which keeps this
|
|
204
312
|
// sound.
|
|
205
|
-
let
|
|
313
|
+
let db = database_inner(ruby, rb_self)?;
|
|
206
314
|
let exec_result = without_gvl(move || {
|
|
207
|
-
let db = InnerDatabase::new(store);
|
|
208
315
|
let options = ExecuteOptions {
|
|
209
316
|
format: ResultFormat::RowArrays,
|
|
210
317
|
};
|
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.4.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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|