lora-ruby

Ruby bindings for the Lora in-memory graph engine. Ships a native extension built with Magnus on top of rb-sys so the Rust engine runs in-process — no separate server, no socket hop.

Status: prototype / feasibility check. Published source gem on RubyGems; precompiled platform gems are built for the supported targets (see "Release" below).

Install

gem install lora-ruby
# or in a Gemfile
gem "lora-ruby"

require "lora_ruby" loads the native extension from lib/lora_ruby/lora_ruby.{so,bundle,dll}. If a precompiled gem for your platform exists on RubyGems, the install is a direct download; if not, the source gem is built locally with cargo and a stable Rust toolchain (1.87+).

Usage

require "lora_ruby"

db = LoraRuby::Database.create

db.execute("CREATE (:Person {name: $n, age: $a})", { n: "Alice", a: 30 })

result = db.execute("MATCH (n:Person) RETURN n")
result["rows"].each do |row|
  n = row["n"]
  puts n["properties"]["name"] if LoraRuby.node?(n)
end

Params

execute accepts a second argument — either nil or a Hash keyed by parameter name (String or Symbol). Values can be any of:

  • nil, true, false, Integer, Float, String, Symbol (stringified)
  • Array of the above (recursive)
  • Hash keyed by String/Symbol with the above values (recursive)
  • Tagged temporal/spatial Hashes produced by the constructors below

Module shape — why LoraRuby::Database?

Three reasonable options were considered:

  • LoraRuby::Database — matches the gem filename; scope is obvious.
  • LoraDB::Database — matches the brand (loradb.com).
  • Lora::Database — shortest, but collides with arbitrary "lora" apps.

We went with LoraRuby::Database for symmetry with the Python binding's lora_python.Database and because it mirrors the require "lora_ruby" path exactly. The gem name on RubyGems is lora-ruby (hyphen); the Ruby constant follows Rubocop convention (CamelCase, no hyphen).

Public API

LoraRuby::Database.create            # -> Database
LoraRuby::Database.new               # -> Database  (alias of .create)

db.execute(query, params = nil)       # -> { "columns" => [...], "rows" => [...] }
db.clear                              # -> nil
db.node_count                         # -> Integer
db.relationship_count                 # -> Integer

LoraRuby::VERSION                    # gem version

Result shape:

{
  "columns" => ["name"],
  "rows"    => [{ "name" => "Alice" }],
}

Hash keys on the output are always strings, matching the lora-node, lora-wasm, and lora-python bindings. Input Hashes accept either symbol or string keys — both work for param names and for tagged constructor Hashes like point/date/...

Typed value model

Identical contract to the other bindings:

Ruby shape Lora value
nil, true/false, Integer, Float, String scalars
Array, Hash collections
{"kind" => "node", "id", "labels", "properties"} node
{"kind" => "relationship", "id", …} relationship
{"kind" => "path", "nodes" => [...], "rels" => [...]} path
{"kind" => "date", "iso" => "YYYY-MM-DD"} (and time, …) temporal
point Hashes (below) point

Points are returned as Hashes keyed on their CRS:

SRID Hash
7203 {"kind"=>"point","srid"=>7203,"crs"=>"cartesian","x","y"}
9157 {"kind"=>"point","srid"=>9157,"crs"=>"cartesian-3D","x","y","z"}
4326 {"kind"=>"point","srid"=>4326,"crs"=>"WGS-84-2D","x","y","longitude","latitude"}
4979 {"kind"=>"point","srid"=>4979,"crs"=>"WGS-84-3D","x","y","z","longitude","latitude","height"}

Constructors and guards

Re-exported on both LoraRuby and LoraRuby::Types:

  • Constructors: date, time, localtime, datetime, localdatetime, duration, cartesian, cartesian_3d, wgs84, wgs84_3d.
  • Guards: node?, relationship?, path?, point?, temporal?.
db.execute(
  "CREATE (:Event {on: $d, at: $c})",
  { d: LoraRuby.date("2025-03-14"), c: LoraRuby.cartesian(1.5, 2.5) },
)

Errors

  • LoraRuby::Error — base class (extends StandardError).
  • LoraRuby::QueryError — parse / analyze / execute failure.
  • LoraRuby::InvalidParamsError — a parameter value couldn't be mapped.

Concurrency (GVL release)

Database#execute calls rb_thread_call_without_gvl, so other Ruby threads run while the engine is busy. Concurrent queries against the same Database serialise on an internal Mutex; parallel queries against different Database instances have no shared state.

The engine has no cancellation hook, so we pass a NULL unblock function. A thread interrupted mid-query (Thread#kill) will observe the interrupt after the current query finishes. Keep queries short if you rely on cooperative cancellation.

Local development

cd crates/lora-ruby
bundle install
bundle exec rake compile        # cargo build → lib/lora_ruby/lora_ruby.<ext>
bundle exec rake test           # minitest
bundle exec rake build          # pkg/lora-ruby-<version>.gem

rake compile drives cargo through rb_sys/extensiontask; it is what gem install runs on end-user machines that don't have a precompiled platform gem.

Architecture

lora-database (Rust, embedded)
   └── crates/lora-ruby/                (gem root + cargo crate)
          ├── Cargo.toml                 Rust workspace member
          ├── extconf.rb                 rb-sys / mkmf entry point
          ├── src/lib.rs                 <- Magnus / rb-sys bindings
          └── lib/lora_ruby/
                 ├── lora_ruby.<ext>      (native, built by rake compile)
                 ├── types.rb             tagged-dict constructors + guards
                 └── version.rb           gem version

rb-sys' convention keeps Cargo.toml and extconf.rb side by side so the cargo manifest directory IS the gem root. That makes the crate a first-class Cargo workspace member (shared Cargo.lock, target/, cargo check --workspace coverage) without a nested ext/<name>/ directory.

Release

Source gem is always built. Precompiled platform gems are emitted via rb_sys/cross for {x86_64,aarch64}-linux, {x86_64,arm64}-darwin, and x64-mingw-ucrt. See .github/workflows/packages-release.yml (Ruby section) and RELEASING.md.