lora-ruby 0.6.0 → 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.
data/src/to_ruby.rs ADDED
@@ -0,0 +1,243 @@
1
+ //! `LoraValue` → Ruby conversion.
2
+ //!
3
+ //! Primitives map to Ruby natives (`Integer`, `Float`, `String`, …);
4
+ //! graph, temporal, and spatial values become tagged `Hash`es with a
5
+ //! `"kind"` discriminator, matching the shared cross-binding contract.
6
+
7
+ use magnus::{prelude::*, Error as MagnusError, RHash, Ruby, Value};
8
+
9
+ use lora_database::{LoraValue, PlanTreeNode, QueryPlan, QueryProfile};
10
+ use lora_store::{LoraBinary, LoraPoint, LoraVector, VectorValues};
11
+
12
+ pub(crate) fn query_plan_to_ruby(ruby: &Ruby, plan: &QueryPlan) -> Result<RHash, MagnusError> {
13
+ let out = ruby.hash_new();
14
+ out.aset(ruby.str_new("query"), ruby.str_new(&plan.query))?;
15
+ out.aset(ruby.str_new("shape"), ruby.str_new(plan.shape.as_str()))?;
16
+ let cols = ruby.ary_new();
17
+ for c in &plan.result_columns {
18
+ cols.push(ruby.str_new(c))?;
19
+ }
20
+ out.aset(ruby.str_new("result_columns"), cols)?;
21
+ out.aset(
22
+ ruby.str_new("tree"),
23
+ plan_tree_node_to_ruby(ruby, &plan.tree.root)?,
24
+ )?;
25
+ Ok(out)
26
+ }
27
+
28
+ pub(crate) fn query_profile_to_ruby(
29
+ ruby: &Ruby,
30
+ profile: &QueryProfile,
31
+ ) -> Result<RHash, MagnusError> {
32
+ let out = ruby.hash_new();
33
+ out.aset(
34
+ ruby.str_new("plan"),
35
+ query_plan_to_ruby(ruby, &profile.plan)?,
36
+ )?;
37
+
38
+ let metrics = ruby.hash_new();
39
+ metrics.aset(
40
+ ruby.str_new("total_elapsed_ns"),
41
+ profile.metrics.total_elapsed_ns,
42
+ )?;
43
+ metrics.aset(ruby.str_new("total_rows"), profile.metrics.total_rows)?;
44
+ metrics.aset(ruby.str_new("mutated"), profile.metrics.mutated)?;
45
+
46
+ let per_op = ruby.hash_new();
47
+ for (id, op) in &profile.metrics.per_operator {
48
+ let entry = ruby.hash_new();
49
+ entry.aset(ruby.str_new("rows"), op.rows)?;
50
+ entry.aset(ruby.str_new("db_hits"), op.db_hits)?;
51
+ entry.aset(ruby.str_new("elapsed_ns"), op.elapsed_ns)?;
52
+ entry.aset(ruby.str_new("next_calls"), op.next_calls)?;
53
+ per_op.aset(*id as u64, entry)?;
54
+ }
55
+ metrics.aset(ruby.str_new("per_operator"), per_op)?;
56
+
57
+ out.aset(ruby.str_new("metrics"), metrics)?;
58
+ Ok(out)
59
+ }
60
+
61
+ fn plan_tree_node_to_ruby(ruby: &Ruby, node: &PlanTreeNode) -> Result<Value, MagnusError> {
62
+ let out = ruby.hash_new();
63
+ out.aset(ruby.str_new("id"), node.id as u64)?;
64
+ out.aset(ruby.str_new("operator"), ruby.str_new(&node.operator))?;
65
+ let details = ruby.hash_new();
66
+ for (k, v) in &node.details {
67
+ details.aset(ruby.str_new(k), ruby.str_new(v))?;
68
+ }
69
+ out.aset(ruby.str_new("details"), details)?;
70
+ match node.estimated_rows {
71
+ Some(r) => out.aset(ruby.str_new("estimated_rows"), r)?,
72
+ None => out.aset(ruby.str_new("estimated_rows"), ruby.qnil())?,
73
+ }
74
+ let children = ruby.ary_new();
75
+ for child in &node.children {
76
+ children.push(plan_tree_node_to_ruby(ruby, child)?)?;
77
+ }
78
+ out.aset(ruby.str_new("children"), children)?;
79
+ Ok(out.as_value())
80
+ }
81
+
82
+ pub(crate) fn lora_value_to_ruby(ruby: &Ruby, value: &LoraValue) -> Result<Value, MagnusError> {
83
+ match value {
84
+ LoraValue::Null => Ok(ruby.qnil().as_value()),
85
+ LoraValue::Bool(b) => Ok(if *b {
86
+ ruby.qtrue().as_value()
87
+ } else {
88
+ ruby.qfalse().as_value()
89
+ }),
90
+ LoraValue::Int(i) => Ok(ruby.integer_from_i64(*i).as_value()),
91
+ LoraValue::Float(f) => Ok(ruby.float_from_f64(*f).as_value()),
92
+ LoraValue::String(s) => Ok(ruby.str_new(s).as_value()),
93
+ LoraValue::List(items) => {
94
+ let arr = ruby.ary_new();
95
+ for item in items {
96
+ arr.push(lora_value_to_ruby(ruby, item)?)?;
97
+ }
98
+ Ok(arr.as_value())
99
+ }
100
+ LoraValue::Map(m) => {
101
+ let h = ruby.hash_new();
102
+ for (k, v) in m {
103
+ h.aset(ruby.str_new(k), lora_value_to_ruby(ruby, v)?)?;
104
+ }
105
+ Ok(h.as_value())
106
+ }
107
+ LoraValue::Node(id) => {
108
+ let h = ruby.hash_new();
109
+ h.aset(ruby.str_new("kind"), ruby.str_new("node"))?;
110
+ h.aset(ruby.str_new("id"), ruby.integer_from_i64(*id as i64))?;
111
+ h.aset(ruby.str_new("labels"), ruby.ary_new())?;
112
+ h.aset(ruby.str_new("properties"), ruby.hash_new())?;
113
+ Ok(h.as_value())
114
+ }
115
+ LoraValue::Relationship(id) => {
116
+ let h = ruby.hash_new();
117
+ h.aset(ruby.str_new("kind"), ruby.str_new("relationship"))?;
118
+ h.aset(ruby.str_new("id"), ruby.integer_from_i64(*id as i64))?;
119
+ Ok(h.as_value())
120
+ }
121
+ LoraValue::Path(p) => {
122
+ let h = ruby.hash_new();
123
+ h.aset(ruby.str_new("kind"), ruby.str_new("path"))?;
124
+ let nodes = ruby.ary_new();
125
+ for n in &p.nodes {
126
+ nodes.push(ruby.integer_from_i64(*n as i64))?;
127
+ }
128
+ let rels = ruby.ary_new();
129
+ for r in &p.rels {
130
+ rels.push(ruby.integer_from_i64(*r as i64))?;
131
+ }
132
+ h.aset(ruby.str_new("nodes"), nodes)?;
133
+ h.aset(ruby.str_new("rels"), rels)?;
134
+ Ok(h.as_value())
135
+ }
136
+ LoraValue::Date(v) => tagged_iso(ruby, "date", v.to_string()),
137
+ LoraValue::Time(v) => tagged_iso(ruby, "time", v.to_string()),
138
+ LoraValue::LocalTime(v) => tagged_iso(ruby, "localtime", v.to_string()),
139
+ LoraValue::DateTime(v) => tagged_iso(ruby, "datetime", v.to_string()),
140
+ LoraValue::LocalDateTime(v) => tagged_iso(ruby, "localdatetime", v.to_string()),
141
+ LoraValue::Duration(v) => tagged_iso(ruby, "duration", v.to_string()),
142
+ LoraValue::Point(p) => point_to_ruby(ruby, p),
143
+ LoraValue::Vector(v) => vector_to_ruby(ruby, v),
144
+ LoraValue::Binary(v) => binary_to_ruby(ruby, v),
145
+ }
146
+ }
147
+
148
+ fn binary_to_ruby(ruby: &Ruby, value: &LoraBinary) -> Result<Value, MagnusError> {
149
+ let h = ruby.hash_new();
150
+ h.aset(ruby.str_new("kind"), ruby.str_new("binary"))?;
151
+ h.aset(
152
+ ruby.str_new("length"),
153
+ ruby.integer_from_i64(value.len() as i64),
154
+ )?;
155
+ let segments = ruby.ary_new();
156
+ for segment in value.segments() {
157
+ segments.push(ruby.str_from_slice(segment))?;
158
+ }
159
+ h.aset(ruby.str_new("segments"), segments)?;
160
+ Ok(h.as_value())
161
+ }
162
+
163
+ fn vector_to_ruby(ruby: &Ruby, v: &LoraVector) -> Result<Value, MagnusError> {
164
+ let h = ruby.hash_new();
165
+ h.aset(ruby.str_new("kind"), ruby.str_new("vector"))?;
166
+ h.aset(
167
+ ruby.str_new("dimension"),
168
+ ruby.integer_from_i64(v.dimension as i64),
169
+ )?;
170
+ h.aset(
171
+ ruby.str_new("coordinateType"),
172
+ ruby.str_new(v.coordinate_type().as_str()),
173
+ )?;
174
+
175
+ let values = ruby.ary_new();
176
+ match &v.values {
177
+ VectorValues::Float64(vs) => {
178
+ for x in vs {
179
+ values.push(ruby.float_from_f64(*x))?;
180
+ }
181
+ }
182
+ VectorValues::Float32(vs) => {
183
+ for x in vs {
184
+ values.push(ruby.float_from_f64(*x as f64))?;
185
+ }
186
+ }
187
+ VectorValues::Integer64(vs) => {
188
+ for x in vs {
189
+ values.push(ruby.integer_from_i64(*x))?;
190
+ }
191
+ }
192
+ VectorValues::Integer32(vs) => {
193
+ for x in vs {
194
+ values.push(ruby.integer_from_i64(*x as i64))?;
195
+ }
196
+ }
197
+ VectorValues::Integer16(vs) => {
198
+ for x in vs {
199
+ values.push(ruby.integer_from_i64(*x as i64))?;
200
+ }
201
+ }
202
+ VectorValues::Integer8(vs) => {
203
+ for x in vs {
204
+ values.push(ruby.integer_from_i64(*x as i64))?;
205
+ }
206
+ }
207
+ }
208
+ h.aset(ruby.str_new("values"), values)?;
209
+ Ok(h.as_value())
210
+ }
211
+
212
+ fn tagged_iso(ruby: &Ruby, kind: &str, iso: String) -> Result<Value, MagnusError> {
213
+ let h: RHash = ruby.hash_new();
214
+ h.aset(ruby.str_new("kind"), ruby.str_new(kind))?;
215
+ h.aset(ruby.str_new("iso"), ruby.str_new(&iso))?;
216
+ Ok(h.as_value())
217
+ }
218
+
219
+ /// Render a `LoraPoint` into the canonical external point shape — kept
220
+ /// 1:1 aligned with the `LoraPoint` union emitted by `lora-node` /
221
+ /// `lora-wasm` / `lora-python`.
222
+ fn point_to_ruby(ruby: &Ruby, p: &LoraPoint) -> Result<Value, MagnusError> {
223
+ let h = ruby.hash_new();
224
+ h.aset(ruby.str_new("kind"), ruby.str_new("point"))?;
225
+ h.aset(ruby.str_new("srid"), ruby.integer_from_i64(p.srid as i64))?;
226
+ h.aset(ruby.str_new("crs"), ruby.str_new(p.crs_name()))?;
227
+ h.aset(ruby.str_new("x"), ruby.float_from_f64(p.x))?;
228
+ h.aset(ruby.str_new("y"), ruby.float_from_f64(p.y))?;
229
+ if let Some(z) = p.z {
230
+ h.aset(ruby.str_new("z"), ruby.float_from_f64(z))?;
231
+ }
232
+ if p.is_geographic() {
233
+ h.aset(
234
+ ruby.str_new("longitude"),
235
+ ruby.float_from_f64(p.longitude()),
236
+ )?;
237
+ h.aset(ruby.str_new("latitude"), ruby.float_from_f64(p.latitude()))?;
238
+ if let Some(height) = p.height() {
239
+ h.aset(ruby.str_new("height"), ruby.float_from_f64(height))?;
240
+ }
241
+ }
242
+ Ok(h.as_value())
243
+ }
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.6.0
4
+ version: 0.8.4
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-29 00:00:00.000000000 Z
11
+ date: 2026-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -83,15 +83,19 @@ files:
83
83
  - lib/lora_ruby.rb
84
84
  - lib/lora_ruby/types.rb
85
85
  - lib/lora_ruby/version.rb
86
+ - src/errors.rs
87
+ - src/from_ruby.rs
88
+ - src/gvl.rs
86
89
  - src/lib.rs
90
+ - src/to_ruby.rs
87
91
  homepage: https://github.com/lora-db/lora
88
92
  licenses:
89
93
  - BUSL-1.1
90
94
  metadata:
91
95
  homepage_uri: https://github.com/lora-db/lora
92
- source_code_uri: https://github.com/lora-db/lora/tree/main/crates/lora-ruby
96
+ source_code_uri: https://github.com/lora-db/lora/tree/main/crates/bindings/lora-ruby
93
97
  bug_tracker_uri: https://github.com/lora-db/lora/issues
94
- documentation_uri: https://github.com/lora-db/lora/blob/main/crates/lora-ruby/README.md
98
+ documentation_uri: https://github.com/lora-db/lora/blob/main/crates/bindings/lora-ruby/README.md
95
99
  rubygems_mfa_required: 'true'
96
100
  post_install_message:
97
101
  rdoc_options: []