lora-ruby 0.5.6 → 0.8.4
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 +54 -11
- data/extconf.rb +1 -1
- 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/errors.rs +74 -0
- data/src/from_ruby.rs +419 -0
- data/src/gvl.rs +55 -0
- data/src/lib.rs +266 -559
- data/src/to_ruby.rs +243 -0
- metadata +8 -4
data/src/lib.rs
CHANGED
|
@@ -14,24 +14,28 @@
|
|
|
14
14
|
//! tagged `Hash`es (string keys) with a `"kind"` discriminator.
|
|
15
15
|
|
|
16
16
|
use std::collections::BTreeMap;
|
|
17
|
-
use std::ffi::c_void;
|
|
18
|
-
use std::mem::MaybeUninit;
|
|
19
17
|
use std::sync::{Arc, Mutex};
|
|
20
18
|
|
|
21
19
|
use magnus::{
|
|
22
|
-
function, method, prelude::*,
|
|
23
|
-
|
|
20
|
+
function, method, prelude::*, value::ReprValue, Error as MagnusError, RHash, RString, Ruby,
|
|
21
|
+
Value,
|
|
24
22
|
};
|
|
25
23
|
|
|
26
24
|
use lora_database::{
|
|
27
|
-
Database as InnerDatabase, DatabaseOpenOptions, ExecuteOptions, InMemoryGraph,
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
use lora_store::{
|
|
31
|
-
LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint, LoraTime,
|
|
32
|
-
LoraVector, RawCoordinate, VectorCoordinateType, VectorValues,
|
|
25
|
+
Database as InnerDatabase, DatabaseOpenOptions, ExecuteOptions, InMemoryGraph, QueryResult,
|
|
26
|
+
ResultFormat, SnapshotConfig, SnapshotOptions, WalConfig,
|
|
33
27
|
};
|
|
34
28
|
|
|
29
|
+
mod errors;
|
|
30
|
+
mod from_ruby;
|
|
31
|
+
mod gvl;
|
|
32
|
+
mod to_ruby;
|
|
33
|
+
|
|
34
|
+
use errors::{invalid_params, query_error, query_error_from_anyhow};
|
|
35
|
+
use from_ruby::{hash_get_any, read_nonnegative_u64, ruby_optional_to_json, ruby_value_to_params};
|
|
36
|
+
use gvl::without_gvl;
|
|
37
|
+
use to_ruby::{lora_value_to_ruby, query_plan_to_ruby, query_profile_to_ruby};
|
|
38
|
+
|
|
35
39
|
// ============================================================================
|
|
36
40
|
// Module / exception registration
|
|
37
41
|
// ============================================================================
|
|
@@ -63,7 +67,10 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
63
67
|
let database = lora_ruby.define_class("Database", ruby.class_object())?;
|
|
64
68
|
database.define_singleton_method("create", function!(database_create, -1))?;
|
|
65
69
|
database.define_singleton_method("new", function!(database_new, -1))?;
|
|
70
|
+
database.define_singleton_method("open_wal", function!(database_open_wal, -1))?;
|
|
66
71
|
database.define_method("execute", method!(database_execute, -1))?;
|
|
72
|
+
database.define_method("explain", method!(database_explain, -1))?;
|
|
73
|
+
database.define_method("profile", method!(database_profile, -1))?;
|
|
67
74
|
database.define_method("clear", method!(database_clear, 0))?;
|
|
68
75
|
database.define_method("close", method!(database_close, 0))?;
|
|
69
76
|
database.define_method("node_count", method!(database_node_count, 0))?;
|
|
@@ -73,8 +80,8 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
73
80
|
)?;
|
|
74
81
|
database.define_method("inspect", method!(database_inspect, 0))?;
|
|
75
82
|
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))?;
|
|
83
|
+
database.define_method("save_snapshot", method!(database_save_snapshot, -1))?;
|
|
84
|
+
database.define_method("load_snapshot", method!(database_load_snapshot, -1))?;
|
|
78
85
|
|
|
79
86
|
// `LoraRuby::VERSION` is owned by `lib/lora_ruby/version.rb` so the
|
|
80
87
|
// gem can expose a version before the native extension compiles
|
|
@@ -84,34 +91,6 @@ fn init(ruby: &Ruby) -> Result<(), MagnusError> {
|
|
|
84
91
|
Ok(())
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// Error lookups
|
|
89
|
-
// ============================================================================
|
|
90
|
-
|
|
91
|
-
fn lora_module(ruby: &Ruby) -> RModule {
|
|
92
|
-
ruby.class_object()
|
|
93
|
-
.const_get::<_, RModule>("LoraRuby")
|
|
94
|
-
.expect("LoraRuby module is defined by `init` before any method runs")
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fn lora_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
98
|
-
// `const_get::<_, ExceptionClass>` converts the stored RClass into
|
|
99
|
-
// an ExceptionClass — this is the sound path, because our subclasses
|
|
100
|
-
// of StandardError retain the exception-class trait on the Ruby
|
|
101
|
-
// side even though `define_class` typed them as RClass.
|
|
102
|
-
lora_module(ruby)
|
|
103
|
-
.const_get::<_, ExceptionClass>(name)
|
|
104
|
-
.unwrap_or_else(|_| ruby.exception_standard_error())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
fn query_error(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
108
|
-
MagnusError::new(lora_error_class(ruby, "QueryError"), msg.into())
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
fn invalid_params(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
|
|
112
|
-
MagnusError::new(lora_error_class(ruby, "InvalidParamsError"), msg.into())
|
|
113
|
-
}
|
|
114
|
-
|
|
115
94
|
// ============================================================================
|
|
116
95
|
// Database
|
|
117
96
|
// ============================================================================
|
|
@@ -126,6 +105,34 @@ struct Database {
|
|
|
126
105
|
db: Mutex<Option<Arc<InnerDatabase<InMemoryGraph>>>>,
|
|
127
106
|
}
|
|
128
107
|
|
|
108
|
+
#[derive(Default)]
|
|
109
|
+
struct RubyDatabaseOpenOptions {
|
|
110
|
+
named: DatabaseOpenOptions,
|
|
111
|
+
has_database_dir: bool,
|
|
112
|
+
wal_dir: Option<String>,
|
|
113
|
+
snapshot_dir: Option<String>,
|
|
114
|
+
snapshot_every_commits: Option<u64>,
|
|
115
|
+
snapshot_keep_old: Option<usize>,
|
|
116
|
+
has_snapshot_codec: bool,
|
|
117
|
+
snapshot_codec: SnapshotOptions,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
impl RubyDatabaseOpenOptions {
|
|
121
|
+
fn has_explicit_wal_options(&self) -> bool {
|
|
122
|
+
self.wal_dir.is_some()
|
|
123
|
+
|| self.snapshot_dir.is_some()
|
|
124
|
+
|| self.snapshot_every_commits.is_some()
|
|
125
|
+
|| self.snapshot_keep_old.is_some()
|
|
126
|
+
|| self.has_snapshot_codec
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn has_snapshot_tuning_options(&self) -> bool {
|
|
130
|
+
self.snapshot_every_commits.is_some()
|
|
131
|
+
|| self.snapshot_keep_old.is_some()
|
|
132
|
+
|| self.has_snapshot_codec
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
impl Database {
|
|
130
137
|
fn from_db(db: Arc<InnerDatabase<InMemoryGraph>>) -> Self {
|
|
131
138
|
Self {
|
|
@@ -148,6 +155,35 @@ fn database_create(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError>
|
|
|
148
155
|
database_new(ruby, args)
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
fn database_open_wal(ruby: &Ruby, args: &[Value]) -> Result<Database, MagnusError> {
|
|
159
|
+
let (wal_dir, mut options) = match args.len() {
|
|
160
|
+
1 | 2 => {
|
|
161
|
+
let wal_dir = RString::try_convert(args[0])?.to_string()?;
|
|
162
|
+
let options = if args.len() == 2 {
|
|
163
|
+
ruby_database_open_options(ruby, RHash::try_convert(args[1])?)?
|
|
164
|
+
} else {
|
|
165
|
+
RubyDatabaseOpenOptions::default()
|
|
166
|
+
};
|
|
167
|
+
(wal_dir, options)
|
|
168
|
+
}
|
|
169
|
+
n => {
|
|
170
|
+
return Err(MagnusError::new(
|
|
171
|
+
ruby.exception_arg_error(),
|
|
172
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
173
|
+
));
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
if options.wal_dir.is_some() {
|
|
177
|
+
return Err(invalid_params(
|
|
178
|
+
ruby,
|
|
179
|
+
"wal_dir must be passed as the first argument to open_wal",
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
options.wal_dir = Some(wal_dir);
|
|
183
|
+
let db = without_gvl(move || open_wal_database(options)).map_err(|e| query_error(ruby, e))?;
|
|
184
|
+
Ok(Database::from_db(db))
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
fn database_clear(ruby: &Ruby, rb_self: &Database) -> Result<(), MagnusError> {
|
|
152
188
|
database_inner(ruby, rb_self)?.clear();
|
|
153
189
|
Ok(())
|
|
@@ -189,27 +225,74 @@ fn database_inspect(rb_self: &Database) -> String {
|
|
|
189
225
|
fn database_save_snapshot(
|
|
190
226
|
ruby: &Ruby,
|
|
191
227
|
rb_self: &Database,
|
|
192
|
-
|
|
228
|
+
args: &[Value],
|
|
193
229
|
) -> Result<RHash, MagnusError> {
|
|
194
|
-
let path =
|
|
230
|
+
let (path, options) = snapshot_file_args(ruby, args)?;
|
|
195
231
|
let db = database_inner(ruby, rb_self)?;
|
|
196
|
-
let meta = without_gvl(move || db.
|
|
197
|
-
.map_err(|e|
|
|
232
|
+
let meta = without_gvl(move || db.save_snapshot_to_with_options(&path, &options))
|
|
233
|
+
.map_err(|e| query_error_from_anyhow(ruby, e))?;
|
|
198
234
|
snapshot_meta_to_rhash(ruby, meta)
|
|
199
235
|
}
|
|
200
236
|
|
|
201
237
|
fn database_load_snapshot(
|
|
202
238
|
ruby: &Ruby,
|
|
203
239
|
rb_self: &Database,
|
|
204
|
-
|
|
240
|
+
args: &[Value],
|
|
205
241
|
) -> Result<RHash, MagnusError> {
|
|
206
|
-
let path =
|
|
242
|
+
let (path, credentials) = snapshot_load_file_args(ruby, args)?;
|
|
207
243
|
let db = database_inner(ruby, rb_self)?;
|
|
208
|
-
let meta =
|
|
209
|
-
|
|
244
|
+
let meta =
|
|
245
|
+
without_gvl(move || db.load_snapshot_from_with_credentials(&path, credentials.as_ref()))
|
|
246
|
+
.map_err(|e| query_error_from_anyhow(ruby, e))?;
|
|
210
247
|
snapshot_meta_to_rhash(ruby, meta)
|
|
211
248
|
}
|
|
212
249
|
|
|
250
|
+
fn snapshot_file_args(
|
|
251
|
+
ruby: &Ruby,
|
|
252
|
+
args: &[Value],
|
|
253
|
+
) -> Result<(String, SnapshotOptions), MagnusError> {
|
|
254
|
+
match args.len() {
|
|
255
|
+
1 | 2 => {
|
|
256
|
+
let path = RString::try_convert(args[0])?.to_string()?;
|
|
257
|
+
let json = if args.len() == 2 {
|
|
258
|
+
ruby_optional_to_json(ruby, args[1])?
|
|
259
|
+
} else {
|
|
260
|
+
None
|
|
261
|
+
};
|
|
262
|
+
let options = lora_database::snapshot_options_from_json(json)
|
|
263
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot options: {e}")))?;
|
|
264
|
+
Ok((path, options))
|
|
265
|
+
}
|
|
266
|
+
n => Err(MagnusError::new(
|
|
267
|
+
ruby.exception_arg_error(),
|
|
268
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
269
|
+
)),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn snapshot_load_file_args(
|
|
274
|
+
ruby: &Ruby,
|
|
275
|
+
args: &[Value],
|
|
276
|
+
) -> Result<(String, Option<lora_database::SnapshotCredentials>), MagnusError> {
|
|
277
|
+
match args.len() {
|
|
278
|
+
1 | 2 => {
|
|
279
|
+
let path = RString::try_convert(args[0])?.to_string()?;
|
|
280
|
+
let json = if args.len() == 2 {
|
|
281
|
+
ruby_optional_to_json(ruby, args[1])?
|
|
282
|
+
} else {
|
|
283
|
+
None
|
|
284
|
+
};
|
|
285
|
+
let credentials = lora_database::snapshot_credentials_from_json(json)
|
|
286
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot credentials: {e}")))?;
|
|
287
|
+
Ok((path, credentials))
|
|
288
|
+
}
|
|
289
|
+
n => Err(MagnusError::new(
|
|
290
|
+
ruby.exception_arg_error(),
|
|
291
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
292
|
+
)),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
213
296
|
fn snapshot_meta_to_rhash(
|
|
214
297
|
ruby: &Ruby,
|
|
215
298
|
meta: lora_database::SnapshotMeta,
|
|
@@ -237,16 +320,18 @@ fn snapshot_meta_to_rhash(
|
|
|
237
320
|
fn database_open_args(
|
|
238
321
|
ruby: &Ruby,
|
|
239
322
|
args: &[Value],
|
|
240
|
-
) -> Result<(Option<String>,
|
|
323
|
+
) -> Result<(Option<String>, RubyDatabaseOpenOptions), MagnusError> {
|
|
241
324
|
match args.len() {
|
|
242
|
-
0 => Ok((None,
|
|
325
|
+
0 => Ok((None, RubyDatabaseOpenOptions::default())),
|
|
243
326
|
1 => {
|
|
244
327
|
if args[0].is_nil() {
|
|
245
|
-
Ok((None,
|
|
328
|
+
Ok((None, RubyDatabaseOpenOptions::default()))
|
|
329
|
+
} else if let Ok(hash) = RHash::try_convert(args[0]) {
|
|
330
|
+
Ok((None, ruby_database_open_options(ruby, hash)?))
|
|
246
331
|
} else {
|
|
247
332
|
Ok((
|
|
248
333
|
Some(RString::try_convert(args[0])?.to_string()?),
|
|
249
|
-
|
|
334
|
+
RubyDatabaseOpenOptions::default(),
|
|
250
335
|
))
|
|
251
336
|
}
|
|
252
337
|
}
|
|
@@ -256,13 +341,7 @@ fn database_open_args(
|
|
|
256
341
|
} else {
|
|
257
342
|
Some(RString::try_convert(args[0])?.to_string()?)
|
|
258
343
|
};
|
|
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
|
-
}
|
|
344
|
+
let options = ruby_database_open_options(ruby, RHash::try_convert(args[1])?)?;
|
|
266
345
|
Ok((database_name, options))
|
|
267
346
|
}
|
|
268
347
|
n => Err(MagnusError::new(
|
|
@@ -272,13 +351,94 @@ fn database_open_args(
|
|
|
272
351
|
}
|
|
273
352
|
}
|
|
274
353
|
|
|
354
|
+
fn ruby_database_open_options(
|
|
355
|
+
ruby: &Ruby,
|
|
356
|
+
hash: RHash,
|
|
357
|
+
) -> Result<RubyDatabaseOpenOptions, MagnusError> {
|
|
358
|
+
let mut options = RubyDatabaseOpenOptions::default();
|
|
359
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["database_dir", "databaseDir"]) {
|
|
360
|
+
options.named.database_dir = RString::try_convert(dir)?.to_string()?.into();
|
|
361
|
+
options.has_database_dir = true;
|
|
362
|
+
}
|
|
363
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["wal_dir", "walDir"]) {
|
|
364
|
+
options.wal_dir = Some(RString::try_convert(dir)?.to_string()?);
|
|
365
|
+
}
|
|
366
|
+
if let Some(dir) = hash_get_any(ruby, hash, &["snapshot_dir", "snapshotDir"]) {
|
|
367
|
+
options.snapshot_dir = Some(RString::try_convert(dir)?.to_string()?);
|
|
368
|
+
}
|
|
369
|
+
if let Some(value) = hash_get_any(
|
|
370
|
+
ruby,
|
|
371
|
+
hash,
|
|
372
|
+
&["snapshot_every_commits", "snapshotEveryCommits"],
|
|
373
|
+
) {
|
|
374
|
+
options.snapshot_every_commits = Some(read_nonnegative_u64(ruby, value)?);
|
|
375
|
+
}
|
|
376
|
+
if let Some(value) = hash_get_any(ruby, hash, &["snapshot_keep_old", "snapshotKeepOld"]) {
|
|
377
|
+
let keep_old = read_nonnegative_u64(ruby, value)?;
|
|
378
|
+
options.snapshot_keep_old = Some(
|
|
379
|
+
usize::try_from(keep_old)
|
|
380
|
+
.map_err(|_| invalid_params(ruby, "snapshot_keep_old does not fit in usize"))?,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if let Some(value) = hash_get_any(ruby, hash, &["snapshot_options", "snapshotOptions"]) {
|
|
384
|
+
let json = ruby_optional_to_json(ruby, value)?;
|
|
385
|
+
options.has_snapshot_codec = true;
|
|
386
|
+
options.snapshot_codec = lora_database::snapshot_options_from_json(json)
|
|
387
|
+
.map_err(|e| invalid_params(ruby, format!("invalid snapshot options: {e}")))?;
|
|
388
|
+
}
|
|
389
|
+
Ok(options)
|
|
390
|
+
}
|
|
391
|
+
|
|
275
392
|
fn open_database(
|
|
276
393
|
database_name: Option<String>,
|
|
277
|
-
options:
|
|
394
|
+
options: RubyDatabaseOpenOptions,
|
|
278
395
|
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
396
|
+
if options.has_explicit_wal_options() {
|
|
397
|
+
return Err(
|
|
398
|
+
"wal_dir/snapshot_dir are not valid for Database.create; use Database.open_wal"
|
|
399
|
+
.to_string(),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
279
402
|
let db = match database_name {
|
|
280
|
-
Some(name) => InnerDatabase::open_named(name, options).map_err(|e| e.to_string())?,
|
|
281
|
-
None =>
|
|
403
|
+
Some(name) => InnerDatabase::open_named(name, options.named).map_err(|e| e.to_string())?,
|
|
404
|
+
None => {
|
|
405
|
+
if options.has_database_dir {
|
|
406
|
+
return Err("database_name is required when database_dir is provided".to_string());
|
|
407
|
+
}
|
|
408
|
+
InnerDatabase::in_memory()
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
Ok(Arc::new(db))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn open_wal_database(
|
|
415
|
+
options: RubyDatabaseOpenOptions,
|
|
416
|
+
) -> Result<Arc<InnerDatabase<InMemoryGraph>>, String> {
|
|
417
|
+
if options.has_database_dir {
|
|
418
|
+
return Err("database_dir is not valid for Database.open_wal".to_string());
|
|
419
|
+
}
|
|
420
|
+
let has_snapshot_tuning = options.has_snapshot_tuning_options();
|
|
421
|
+
if options.snapshot_dir.is_none() && has_snapshot_tuning {
|
|
422
|
+
return Err(
|
|
423
|
+
"snapshot_dir is required when managed snapshot options are provided".to_string(),
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
let wal_dir = options
|
|
427
|
+
.wal_dir
|
|
428
|
+
.ok_or_else(|| "wal_dir is required for Database.open_wal".to_string())?;
|
|
429
|
+
let wal_config = WalConfig::enabled(wal_dir);
|
|
430
|
+
let db = if let Some(snapshot_dir) = options.snapshot_dir {
|
|
431
|
+
let mut snapshots = SnapshotConfig::enabled(snapshot_dir)
|
|
432
|
+
.keep_old(options.snapshot_keep_old.unwrap_or(1))
|
|
433
|
+
.codec(options.snapshot_codec);
|
|
434
|
+
if let Some(every) = options.snapshot_every_commits {
|
|
435
|
+
if every != 0 {
|
|
436
|
+
snapshots = snapshots.every_commits(every);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
InnerDatabase::open_with_wal_snapshots(wal_config, snapshots).map_err(|e| e.to_string())?
|
|
440
|
+
} else {
|
|
441
|
+
InnerDatabase::open_with_wal(wal_config).map_err(|e| e.to_string())?
|
|
282
442
|
};
|
|
283
443
|
Ok(Arc::new(db))
|
|
284
444
|
}
|
|
@@ -344,7 +504,7 @@ fn database_execute(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
|
|
|
344
504
|
let row_arrays = match exec_result {
|
|
345
505
|
Ok(QueryResult::RowArrays(r)) => r,
|
|
346
506
|
Ok(_) => return Err(query_error(ruby, "expected RowArrays result")),
|
|
347
|
-
Err(e) => return Err(
|
|
507
|
+
Err(e) => return Err(query_error_from_anyhow(ruby, e)),
|
|
348
508
|
};
|
|
349
509
|
|
|
350
510
|
let out = ruby.hash_new();
|
|
@@ -366,507 +526,54 @@ fn database_execute(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<R
|
|
|
366
526
|
Ok(out)
|
|
367
527
|
}
|
|
368
528
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
match
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
ruby.qtrue().as_value()
|
|
378
|
-
} else {
|
|
379
|
-
ruby.qfalse().as_value()
|
|
380
|
-
}),
|
|
381
|
-
LoraValue::Int(i) => Ok(ruby.integer_from_i64(*i).as_value()),
|
|
382
|
-
LoraValue::Float(f) => Ok(ruby.float_from_f64(*f).as_value()),
|
|
383
|
-
LoraValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
384
|
-
LoraValue::List(items) => {
|
|
385
|
-
let arr = ruby.ary_new();
|
|
386
|
-
for item in items {
|
|
387
|
-
arr.push(lora_value_to_ruby(ruby, item)?)?;
|
|
388
|
-
}
|
|
389
|
-
Ok(arr.as_value())
|
|
390
|
-
}
|
|
391
|
-
LoraValue::Map(m) => {
|
|
392
|
-
let h = ruby.hash_new();
|
|
393
|
-
for (k, v) in m {
|
|
394
|
-
h.aset(ruby.str_new(k), lora_value_to_ruby(ruby, v)?)?;
|
|
395
|
-
}
|
|
396
|
-
Ok(h.as_value())
|
|
397
|
-
}
|
|
398
|
-
LoraValue::Node(id) => {
|
|
399
|
-
let h = ruby.hash_new();
|
|
400
|
-
h.aset(ruby.str_new("kind"), ruby.str_new("node"))?;
|
|
401
|
-
h.aset(ruby.str_new("id"), ruby.integer_from_i64(*id as i64))?;
|
|
402
|
-
h.aset(ruby.str_new("labels"), ruby.ary_new())?;
|
|
403
|
-
h.aset(ruby.str_new("properties"), ruby.hash_new())?;
|
|
404
|
-
Ok(h.as_value())
|
|
405
|
-
}
|
|
406
|
-
LoraValue::Relationship(id) => {
|
|
407
|
-
let h = ruby.hash_new();
|
|
408
|
-
h.aset(ruby.str_new("kind"), ruby.str_new("relationship"))?;
|
|
409
|
-
h.aset(ruby.str_new("id"), ruby.integer_from_i64(*id as i64))?;
|
|
410
|
-
Ok(h.as_value())
|
|
411
|
-
}
|
|
412
|
-
LoraValue::Path(p) => {
|
|
413
|
-
let h = ruby.hash_new();
|
|
414
|
-
h.aset(ruby.str_new("kind"), ruby.str_new("path"))?;
|
|
415
|
-
let nodes = ruby.ary_new();
|
|
416
|
-
for n in &p.nodes {
|
|
417
|
-
nodes.push(ruby.integer_from_i64(*n as i64))?;
|
|
418
|
-
}
|
|
419
|
-
let rels = ruby.ary_new();
|
|
420
|
-
for r in &p.rels {
|
|
421
|
-
rels.push(ruby.integer_from_i64(*r as i64))?;
|
|
422
|
-
}
|
|
423
|
-
h.aset(ruby.str_new("nodes"), nodes)?;
|
|
424
|
-
h.aset(ruby.str_new("rels"), rels)?;
|
|
425
|
-
Ok(h.as_value())
|
|
426
|
-
}
|
|
427
|
-
LoraValue::Date(v) => tagged_iso(ruby, "date", v.to_string()),
|
|
428
|
-
LoraValue::Time(v) => tagged_iso(ruby, "time", v.to_string()),
|
|
429
|
-
LoraValue::LocalTime(v) => tagged_iso(ruby, "localtime", v.to_string()),
|
|
430
|
-
LoraValue::DateTime(v) => tagged_iso(ruby, "datetime", v.to_string()),
|
|
431
|
-
LoraValue::LocalDateTime(v) => tagged_iso(ruby, "localdatetime", v.to_string()),
|
|
432
|
-
LoraValue::Duration(v) => tagged_iso(ruby, "duration", v.to_string()),
|
|
433
|
-
LoraValue::Point(p) => point_to_ruby(ruby, p),
|
|
434
|
-
LoraValue::Vector(v) => vector_to_ruby(ruby, v),
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
fn vector_to_ruby(ruby: &Ruby, v: &LoraVector) -> Result<Value, MagnusError> {
|
|
439
|
-
let h = ruby.hash_new();
|
|
440
|
-
h.aset(ruby.str_new("kind"), ruby.str_new("vector"))?;
|
|
441
|
-
h.aset(
|
|
442
|
-
ruby.str_new("dimension"),
|
|
443
|
-
ruby.integer_from_i64(v.dimension as i64),
|
|
444
|
-
)?;
|
|
445
|
-
h.aset(
|
|
446
|
-
ruby.str_new("coordinateType"),
|
|
447
|
-
ruby.str_new(v.coordinate_type().as_str()),
|
|
448
|
-
)?;
|
|
449
|
-
|
|
450
|
-
let values = ruby.ary_new();
|
|
451
|
-
match &v.values {
|
|
452
|
-
VectorValues::Float64(vs) => {
|
|
453
|
-
for x in vs {
|
|
454
|
-
values.push(ruby.float_from_f64(*x))?;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
VectorValues::Float32(vs) => {
|
|
458
|
-
for x in vs {
|
|
459
|
-
values.push(ruby.float_from_f64(*x as f64))?;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
VectorValues::Integer64(vs) => {
|
|
463
|
-
for x in vs {
|
|
464
|
-
values.push(ruby.integer_from_i64(*x))?;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
VectorValues::Integer32(vs) => {
|
|
468
|
-
for x in vs {
|
|
469
|
-
values.push(ruby.integer_from_i64(*x as i64))?;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
VectorValues::Integer16(vs) => {
|
|
473
|
-
for x in vs {
|
|
474
|
-
values.push(ruby.integer_from_i64(*x as i64))?;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
VectorValues::Integer8(vs) => {
|
|
478
|
-
for x in vs {
|
|
479
|
-
values.push(ruby.integer_from_i64(*x as i64))?;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
h.aset(ruby.str_new("values"), values)?;
|
|
484
|
-
Ok(h.as_value())
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
fn tagged_iso(ruby: &Ruby, kind: &str, iso: String) -> Result<Value, MagnusError> {
|
|
488
|
-
let h = ruby.hash_new();
|
|
489
|
-
h.aset(ruby.str_new("kind"), ruby.str_new(kind))?;
|
|
490
|
-
h.aset(ruby.str_new("iso"), ruby.str_new(&iso))?;
|
|
491
|
-
Ok(h.as_value())
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/// Render a `LoraPoint` into the canonical external point shape — kept
|
|
495
|
-
/// 1:1 aligned with the `LoraPoint` union emitted by `lora-node` /
|
|
496
|
-
/// `lora-wasm` / `lora-python`.
|
|
497
|
-
fn point_to_ruby(ruby: &Ruby, p: &LoraPoint) -> Result<Value, MagnusError> {
|
|
498
|
-
let h = ruby.hash_new();
|
|
499
|
-
h.aset(ruby.str_new("kind"), ruby.str_new("point"))?;
|
|
500
|
-
h.aset(ruby.str_new("srid"), ruby.integer_from_i64(p.srid as i64))?;
|
|
501
|
-
h.aset(ruby.str_new("crs"), ruby.str_new(p.crs_name()))?;
|
|
502
|
-
h.aset(ruby.str_new("x"), ruby.float_from_f64(p.x))?;
|
|
503
|
-
h.aset(ruby.str_new("y"), ruby.float_from_f64(p.y))?;
|
|
504
|
-
if let Some(z) = p.z {
|
|
505
|
-
h.aset(ruby.str_new("z"), ruby.float_from_f64(z))?;
|
|
506
|
-
}
|
|
507
|
-
if p.is_geographic() {
|
|
508
|
-
h.aset(
|
|
509
|
-
ruby.str_new("longitude"),
|
|
510
|
-
ruby.float_from_f64(p.longitude()),
|
|
511
|
-
)?;
|
|
512
|
-
h.aset(ruby.str_new("latitude"), ruby.float_from_f64(p.latitude()))?;
|
|
513
|
-
if let Some(height) = p.height() {
|
|
514
|
-
h.aset(ruby.str_new("height"), ruby.float_from_f64(height))?;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
Ok(h.as_value())
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ============================================================================
|
|
521
|
-
// Ruby → LoraValue (params)
|
|
522
|
-
// ============================================================================
|
|
523
|
-
|
|
524
|
-
fn ruby_value_to_params(
|
|
525
|
-
ruby: &Ruby,
|
|
526
|
-
value: Value,
|
|
527
|
-
) -> Result<BTreeMap<String, LoraValue>, MagnusError> {
|
|
528
|
-
let hash = RHash::try_convert(value)
|
|
529
|
-
.map_err(|_| invalid_params(ruby, "params must be a Hash keyed by parameter name"))?;
|
|
530
|
-
hash_to_string_map(ruby, hash)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
fn hash_to_string_map(
|
|
534
|
-
ruby: &Ruby,
|
|
535
|
-
hash: RHash,
|
|
536
|
-
) -> Result<BTreeMap<String, LoraValue>, MagnusError> {
|
|
537
|
-
let mut out = BTreeMap::new();
|
|
538
|
-
let mut inner_err: Option<MagnusError> = None;
|
|
539
|
-
hash.foreach(|k: Value, v: Value| {
|
|
540
|
-
let key = match coerce_key(ruby, k) {
|
|
541
|
-
Ok(s) => s,
|
|
542
|
-
Err(e) => {
|
|
543
|
-
inner_err = Some(e);
|
|
544
|
-
return Ok(ForEach::Stop);
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
match ruby_value_to_lora(ruby, v) {
|
|
548
|
-
Ok(lv) => {
|
|
549
|
-
out.insert(key, lv);
|
|
550
|
-
Ok(ForEach::Continue)
|
|
551
|
-
}
|
|
552
|
-
Err(e) => {
|
|
553
|
-
inner_err = Some(e);
|
|
554
|
-
Ok(ForEach::Stop)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
})?;
|
|
558
|
-
if let Some(e) = inner_err {
|
|
559
|
-
return Err(e);
|
|
560
|
-
}
|
|
561
|
-
Ok(out)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
fn coerce_key(ruby: &Ruby, v: Value) -> Result<String, MagnusError> {
|
|
565
|
-
// Accept both String and Symbol keys — idiomatic Ruby. Reject anything
|
|
566
|
-
// else loudly; silently stringifying would mask caller mistakes.
|
|
567
|
-
if let Ok(s) = RString::try_convert(v) {
|
|
568
|
-
return s.to_string();
|
|
569
|
-
}
|
|
570
|
-
if let Ok(s) = Symbol::try_convert(v) {
|
|
571
|
-
return Ok(s.name()?.into_owned());
|
|
572
|
-
}
|
|
573
|
-
Err(invalid_params(ruby, "param keys must be String or Symbol"))
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
fn ruby_value_to_lora(ruby: &Ruby, v: Value) -> Result<LoraValue, MagnusError> {
|
|
577
|
-
if v.is_nil() {
|
|
578
|
-
return Ok(LoraValue::Null);
|
|
579
|
-
}
|
|
580
|
-
// Check true/false before Integer — Ruby's TrueClass / FalseClass are
|
|
581
|
-
// not Integer subclasses, but bool detection is cleaner first.
|
|
582
|
-
if v.is_kind_of(ruby.class_true_class()) {
|
|
583
|
-
return Ok(LoraValue::Bool(true));
|
|
584
|
-
}
|
|
585
|
-
if v.is_kind_of(ruby.class_false_class()) {
|
|
586
|
-
return Ok(LoraValue::Bool(false));
|
|
587
|
-
}
|
|
588
|
-
// Float MUST be checked before Integer — `Integer::try_convert`
|
|
589
|
-
// succeeds on Float because Ruby's `Float#to_int` (truncating
|
|
590
|
-
// coercion) makes `Float` implicitly convertible. Taking that path
|
|
591
|
-
// would turn `1.5` into `1` silently; callers never want that.
|
|
592
|
-
if let Ok(f) = Float::try_convert(v) {
|
|
593
|
-
return Ok(LoraValue::Float(f.to_f64()));
|
|
594
|
-
}
|
|
595
|
-
if let Ok(i) = Integer::try_convert(v) {
|
|
596
|
-
return match i.to_i64() {
|
|
597
|
-
Ok(n) => Ok(LoraValue::Int(n)),
|
|
598
|
-
Err(_) => Err(invalid_params(
|
|
599
|
-
ruby,
|
|
600
|
-
"integer parameter does not fit in i64",
|
|
601
|
-
)),
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
if let Ok(s) = RString::try_convert(v) {
|
|
605
|
-
return Ok(LoraValue::String(s.to_string()?));
|
|
606
|
-
}
|
|
607
|
-
if let Ok(sym) = Symbol::try_convert(v) {
|
|
608
|
-
// Symbols round-trip as strings — same approach as YAML/JSON
|
|
609
|
-
// mappings. Engine has no dedicated symbol value.
|
|
610
|
-
return Ok(LoraValue::String(sym.name()?.into_owned()));
|
|
611
|
-
}
|
|
612
|
-
if let Ok(arr) = RArray::try_convert(v) {
|
|
613
|
-
let mut out = Vec::with_capacity(arr.len());
|
|
614
|
-
for item in arr.into_iter() {
|
|
615
|
-
out.push(ruby_value_to_lora(ruby, item)?);
|
|
616
|
-
}
|
|
617
|
-
return Ok(LoraValue::List(out));
|
|
618
|
-
}
|
|
619
|
-
if let Ok(hash) = RHash::try_convert(v) {
|
|
620
|
-
return ruby_hash_to_cypher(ruby, hash);
|
|
621
|
-
}
|
|
622
|
-
let class_name = unsafe { v.classname() }.into_owned();
|
|
623
|
-
Err(invalid_params(
|
|
624
|
-
ruby,
|
|
625
|
-
format!("unsupported parameter type: {class_name}"),
|
|
626
|
-
))
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/// A Hash might be a tagged value (date / time / …/ point) or a plain
|
|
630
|
-
/// map. Nodes / relationships / paths are opaque on the engine side and
|
|
631
|
-
/// cannot be reconstructed as params — there's no `"kind" => "node"`
|
|
632
|
-
/// tag handled here.
|
|
633
|
-
fn ruby_hash_to_cypher(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusError> {
|
|
634
|
-
if let Some(kind) = lookup_kind(ruby, hash)? {
|
|
635
|
-
match kind.as_str() {
|
|
636
|
-
"date" => {
|
|
637
|
-
return parse_tagged(ruby, hash, "date", |iso| {
|
|
638
|
-
LoraDate::parse(iso).map(LoraValue::Date)
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
"time" => {
|
|
642
|
-
return parse_tagged(ruby, hash, "time", |iso| {
|
|
643
|
-
LoraTime::parse(iso).map(LoraValue::Time)
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
"localtime" => {
|
|
647
|
-
return parse_tagged(ruby, hash, "localtime", |iso| {
|
|
648
|
-
LoraLocalTime::parse(iso).map(LoraValue::LocalTime)
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
"datetime" => {
|
|
652
|
-
return parse_tagged(ruby, hash, "datetime", |iso| {
|
|
653
|
-
LoraDateTime::parse(iso).map(LoraValue::DateTime)
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
"localdatetime" => {
|
|
657
|
-
return parse_tagged(ruby, hash, "localdatetime", |iso| {
|
|
658
|
-
LoraLocalDateTime::parse(iso).map(LoraValue::LocalDateTime)
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
"duration" => {
|
|
662
|
-
return parse_tagged(ruby, hash, "duration", |iso| {
|
|
663
|
-
LoraDuration::parse(iso).map(LoraValue::Duration)
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
"point" => return build_point(ruby, hash),
|
|
667
|
-
"vector" => return build_vector(ruby, hash),
|
|
668
|
-
_ => { /* fall through to plain-map handling */ }
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
Ok(LoraValue::Map(hash_to_string_map(ruby, hash)?))
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/// Look up `"kind"` (string) or `:kind` (symbol) under either key. Keeps
|
|
676
|
-
/// constructor hashes usable with either Ruby idiom.
|
|
677
|
-
fn lookup_kind(ruby: &Ruby, hash: RHash) -> Result<Option<String>, MagnusError> {
|
|
678
|
-
if let Some(v) = hash.get(ruby.str_new("kind")) {
|
|
679
|
-
return kind_as_string(v).map(Some);
|
|
680
|
-
}
|
|
681
|
-
if let Some(v) = hash.get(ruby.to_symbol("kind")) {
|
|
682
|
-
return kind_as_string(v).map(Some);
|
|
683
|
-
}
|
|
684
|
-
Ok(None)
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
fn kind_as_string(v: Value) -> Result<String, MagnusError> {
|
|
688
|
-
if let Ok(s) = RString::try_convert(v) {
|
|
689
|
-
return s.to_string();
|
|
690
|
-
}
|
|
691
|
-
if let Ok(s) = Symbol::try_convert(v) {
|
|
692
|
-
return Ok(s.name()?.into_owned());
|
|
693
|
-
}
|
|
694
|
-
// Anything else means "not a tagged constructor" — return empty so
|
|
695
|
-
// the caller falls through to plain-map handling instead of raising.
|
|
696
|
-
Ok(String::new())
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
fn parse_tagged(
|
|
700
|
-
ruby: &Ruby,
|
|
701
|
-
hash: RHash,
|
|
702
|
-
tag: &str,
|
|
703
|
-
parse: impl FnOnce(&str) -> Result<LoraValue, String>,
|
|
704
|
-
) -> Result<LoraValue, MagnusError> {
|
|
705
|
-
let iso = read_string(ruby, hash, "iso")?
|
|
706
|
-
.ok_or_else(|| invalid_params(ruby, format!("{tag} value requires iso: String")))?;
|
|
707
|
-
parse(&iso).map_err(|e| invalid_params(ruby, format!("{tag}: {e}")))
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
fn build_point(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusError> {
|
|
711
|
-
let srid = read_u32(ruby, hash, "srid")?.unwrap_or(7203);
|
|
712
|
-
let x = read_f64(ruby, hash, "x")?.ok_or_else(|| invalid_params(ruby, "point.x required"))?;
|
|
713
|
-
let y = read_f64(ruby, hash, "y")?.ok_or_else(|| invalid_params(ruby, "point.y required"))?;
|
|
714
|
-
let z = read_f64(ruby, hash, "z")?;
|
|
715
|
-
Ok(LoraValue::Point(LoraPoint { x, y, z, srid }))
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
fn build_vector(ruby: &Ruby, hash: RHash) -> Result<LoraValue, MagnusError> {
|
|
719
|
-
let dimension = read_i64(ruby, hash, "dimension")?
|
|
720
|
-
.ok_or_else(|| invalid_params(ruby, "vector.dimension required"))?;
|
|
721
|
-
let coordinate_type_name = read_string(ruby, hash, "coordinateType")?
|
|
722
|
-
.ok_or_else(|| invalid_params(ruby, "vector.coordinateType required"))?;
|
|
723
|
-
let coordinate_type = VectorCoordinateType::parse(&coordinate_type_name).ok_or_else(|| {
|
|
724
|
-
invalid_params(
|
|
725
|
-
ruby,
|
|
726
|
-
format!("unknown vector coordinate type '{coordinate_type_name}'"),
|
|
727
|
-
)
|
|
728
|
-
})?;
|
|
729
|
-
let values_value = hash_get_either(ruby, hash, "values")
|
|
730
|
-
.ok_or_else(|| invalid_params(ruby, "vector.values required"))?;
|
|
731
|
-
let arr = RArray::try_convert(values_value)
|
|
732
|
-
.map_err(|_| invalid_params(ruby, "vector.values must be an Array"))?;
|
|
733
|
-
|
|
734
|
-
let mut raw = Vec::with_capacity(arr.len());
|
|
735
|
-
for item in arr.into_iter() {
|
|
736
|
-
if item.is_kind_of(ruby.class_true_class()) || item.is_kind_of(ruby.class_false_class()) {
|
|
737
|
-
return Err(invalid_params(
|
|
738
|
-
ruby,
|
|
739
|
-
"vector.values entries must be numeric",
|
|
740
|
-
));
|
|
741
|
-
}
|
|
742
|
-
if let Ok(f) = Float::try_convert(item) {
|
|
743
|
-
let v = f.to_f64();
|
|
744
|
-
if !v.is_finite() {
|
|
745
|
-
return Err(invalid_params(
|
|
746
|
-
ruby,
|
|
747
|
-
"vector.values cannot be NaN or Infinity",
|
|
748
|
-
));
|
|
749
|
-
}
|
|
750
|
-
raw.push(RawCoordinate::Float(v));
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
if let Ok(i) = Integer::try_convert(item) {
|
|
754
|
-
raw.push(RawCoordinate::Int(i.to_i64()?));
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
return Err(invalid_params(
|
|
758
|
-
ruby,
|
|
759
|
-
"vector.values entries must be numeric",
|
|
760
|
-
));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
let v = LoraVector::try_new(raw, dimension, coordinate_type)
|
|
764
|
-
.map_err(|e| invalid_params(ruby, e.to_string()))?;
|
|
765
|
-
Ok(LoraValue::Vector(v))
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
fn read_i64(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<i64>, MagnusError> {
|
|
769
|
-
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
770
|
-
return Ok(None);
|
|
771
|
-
};
|
|
772
|
-
Ok(Some(Integer::try_convert(v)?.to_i64().map_err(|_| {
|
|
773
|
-
invalid_params(ruby, format!("{key} out of i64 range"))
|
|
774
|
-
})?))
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
// ---- Hash accessors that accept either string or symbol keys ------------
|
|
778
|
-
|
|
779
|
-
fn hash_get_either(ruby: &Ruby, hash: RHash, key: &str) -> Option<Value> {
|
|
780
|
-
if let Some(v) = hash.get(ruby.str_new(key)) {
|
|
781
|
-
return Some(v);
|
|
782
|
-
}
|
|
783
|
-
hash.get(ruby.to_symbol(key))
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
fn read_string(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<String>, MagnusError> {
|
|
787
|
-
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
788
|
-
return Ok(None);
|
|
789
|
-
};
|
|
790
|
-
let s = RString::try_convert(v)?.to_string()?;
|
|
791
|
-
Ok(Some(s))
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
fn read_u32(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<u32>, MagnusError> {
|
|
795
|
-
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
796
|
-
return Ok(None);
|
|
797
|
-
};
|
|
798
|
-
let n = Integer::try_convert(v)?.to_i64()?;
|
|
799
|
-
u32::try_from(n)
|
|
800
|
-
.map(Some)
|
|
801
|
-
.map_err(|_| invalid_params(ruby, "srid out of u32 range"))
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
fn read_f64(ruby: &Ruby, hash: RHash, key: &str) -> Result<Option<f64>, MagnusError> {
|
|
805
|
-
let Some(v) = hash_get_either(ruby, hash, key) else {
|
|
806
|
-
return Ok(None);
|
|
529
|
+
/// `explain(query, params = nil)` — compile a query and return the plan
|
|
530
|
+
/// as a Ruby `Hash` without invoking the executor. Mutating queries
|
|
531
|
+
/// produce no side effects.
|
|
532
|
+
fn database_explain(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<RHash, MagnusError> {
|
|
533
|
+
let (query, params_value) = parse_query_params(ruby, args)?;
|
|
534
|
+
let params_map = match params_value {
|
|
535
|
+
Some(v) => Some(ruby_value_to_params(ruby, v)?),
|
|
536
|
+
None => None,
|
|
807
537
|
};
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
if let Ok(i) = Integer::try_convert(v) {
|
|
814
|
-
return Ok(Some(i.to_i64()? as f64));
|
|
815
|
-
}
|
|
816
|
-
Ok(None)
|
|
538
|
+
let db = database_inner(ruby, rb_self)?;
|
|
539
|
+
let plan = without_gvl(move || db.explain(&query, params_map))
|
|
540
|
+
.map_err(|e| query_error_from_anyhow(ruby, e))?;
|
|
541
|
+
query_plan_to_ruby(ruby, &plan)
|
|
817
542
|
}
|
|
818
543
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
// ============================================================================
|
|
822
|
-
|
|
823
|
-
/// Run `f` with Ruby's Global VM Lock released.
|
|
544
|
+
/// `profile(query, params = nil)` — execute a query and return the plan
|
|
545
|
+
/// plus runtime metrics as a Ruby `Hash`.
|
|
824
546
|
///
|
|
825
|
-
///
|
|
826
|
-
///
|
|
827
|
-
///
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
F: FnOnce() -> R,
|
|
834
|
-
F: Send,
|
|
835
|
-
R: Send,
|
|
836
|
-
{
|
|
837
|
-
struct Data<F, R> {
|
|
838
|
-
func: Option<F>,
|
|
839
|
-
result: MaybeUninit<R>,
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
unsafe extern "C" fn trampoline<F, R>(data: *mut c_void) -> *mut c_void
|
|
843
|
-
where
|
|
844
|
-
F: FnOnce() -> R,
|
|
845
|
-
{
|
|
846
|
-
let data = &mut *(data as *mut Data<F, R>);
|
|
847
|
-
let f = data
|
|
848
|
-
.func
|
|
849
|
-
.take()
|
|
850
|
-
.expect("without_gvl: closure already taken");
|
|
851
|
-
data.result.write(f());
|
|
852
|
-
std::ptr::null_mut()
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
let mut data = Data::<F, R> {
|
|
856
|
-
func: Some(f),
|
|
857
|
-
result: MaybeUninit::uninit(),
|
|
547
|
+
/// **PROFILE executes the query for real.** Mutating queries are
|
|
548
|
+
/// persisted exactly as in `execute`. Use `explain` to inspect a
|
|
549
|
+
/// mutating plan without running it.
|
|
550
|
+
fn database_profile(ruby: &Ruby, rb_self: &Database, args: &[Value]) -> Result<RHash, MagnusError> {
|
|
551
|
+
let (query, params_value) = parse_query_params(ruby, args)?;
|
|
552
|
+
let params_map = match params_value {
|
|
553
|
+
Some(v) => Some(ruby_value_to_params(ruby, v)?),
|
|
554
|
+
None => None,
|
|
858
555
|
};
|
|
556
|
+
let db = database_inner(ruby, rb_self)?;
|
|
557
|
+
let prof = without_gvl(move || db.profile(&query, params_map))
|
|
558
|
+
.map_err(|e| query_error_from_anyhow(ruby, e))?;
|
|
559
|
+
query_profile_to_ruby(ruby, &prof)
|
|
560
|
+
}
|
|
859
561
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
562
|
+
fn parse_query_params(ruby: &Ruby, args: &[Value]) -> Result<(String, Option<Value>), MagnusError> {
|
|
563
|
+
match args.len() {
|
|
564
|
+
1 => Ok((RString::try_convert(args[0])?.to_string()?, None)),
|
|
565
|
+
2 => {
|
|
566
|
+
let q = RString::try_convert(args[0])?.to_string()?;
|
|
567
|
+
let p = if args[1].is_nil() {
|
|
568
|
+
None
|
|
569
|
+
} else {
|
|
570
|
+
Some(args[1])
|
|
571
|
+
};
|
|
572
|
+
Ok((q, p))
|
|
573
|
+
}
|
|
574
|
+
n => Err(MagnusError::new(
|
|
575
|
+
ruby.exception_arg_error(),
|
|
576
|
+
format!("wrong number of arguments (given {n}, expected 1..2)"),
|
|
577
|
+
)),
|
|
871
578
|
}
|
|
872
579
|
}
|