lora-ruby

Ruby bindings for the Lora 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

Initialization rule:

scratch = LoraRuby::Database.create         # in-memory
persistent = LoraRuby::Database.create("./app") # persistent: directory string

If you want persistence, pass a directory string to LoraRuby::Database.create(...) or LoraRuby::Database.new(...).

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(wal_dir = nil)  # -> Database
LoraRuby::Database.new(wal_dir = nil)     # -> 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.

Persistence

LoraRuby::Database.create("./app") and LoraRuby::Database.new("./app") open or create a WAL-backed persistent database rooted at that directory. Reopening the same path replays committed writes before returning the handle.

Call db.close before reopening the same WAL directory inside one process.

This first Ruby persistence slice intentionally stays small: the binding exposes WAL-backed initialization plus the existing snapshot APIs, but not checkpoint, truncate, status, or sync-mode controls.

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.