Skip to main content

Using LoraDB in Ruby

Overview

lora-ruby is a native extension built with Magnus on top of rb-sys. The engine runs in-process — no separate server, no socket hop. Values follow the same tagged model as the Node, Python, WASM, and Go bindings (primitives pass through; nodes, relationships, paths, temporals, and points come back as Hashes with a "kind" discriminator).

Installation / Setup

Requirements

  • Ruby 3.1+
  • Rust toolchain (rustup) — only needed when no precompiled platform gem is available for your platform

Install

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

If a precompiled platform gem exists for your {os, arch, ruby ABI} the install is a direct download; otherwise gem install falls through to a source build via cargo + rb-sys.

Creating a Client / Connection

require "lora_ruby"

db = LoraRuby::Database.create

LoraRuby::Database.create and LoraRuby::Database.new are the same constructor — both return a ready-to-use handle over an empty in-memory graph.

Running Your First Query

require "lora_ruby"

db = LoraRuby::Database.create

db.execute("CREATE (:Person {name: 'Ada', born: 1815})")

result = db.execute("MATCH (p:Person) RETURN p.name AS name, p.born AS born")

puts result["rows"]
# [{"name"=>"Ada", "born"=>1815}]

Examples

Parameterised query

result = db.execute(
"MATCH (p:Person) WHERE p.name = $name RETURN p.name AS name",
{ name: "Ada" },
)

Params accept String or Symbol keys. Ruby values map automatically: nilNull, true/falseBoolean, IntegerInteger, FloatFloat, String/SymbolString, ArrayList, HashMap. Use the tagged helpers for dates, durations, and points — see typed helpers below.

Explain and profile

explain and profile are binding methods, not Cypher keywords in the query string. db.explain(...) compiles the query and returns the physical plan without running the executor:

plan = db.explain(
"MATCH (p:Person) WHERE p.name = $name RETURN p",
{ name: "Ada" },
)

puts plan["shape"] # "readOnly" or "mutating"
puts plan["result_columns"] # ["p"]
puts plan["tree"]["operator"] # top-level physical operator

Ruby uses snake_case keys for the explain/profile envelopes: result_columns, estimated_rows, total_elapsed_ns, and per_operator. Plan details values are human-readable and opaque; avoid parsing them programmatically.

db.profile(...) runs the query and returns the plan plus runtime metrics:

profile = db.profile(
"MATCH (p:Person) WHERE p.name = $name RETURN p",
{ name: "Ada" },
)

puts profile["metrics"]["total_elapsed_ns"]
puts profile["metrics"]["total_rows"]
puts profile["metrics"]["mutated"]
puts profile["metrics"]["per_operator"]
profile executes the query

Mutating queries passed to profile produce the same side effects as execute. Use explain to inspect a mutating CREATE, MERGE, SET, DELETE, or REMOVE plan without changing the graph.

Both methods accept the same parameter values as execute, including tagged helper Hashes for temporal, spatial, vector, and binary values. Graph Hashes such as returned nodes are result values; for input, pass property values or typed helper values:

params = {
since: LoraRuby.date("1800-01-01"),
near: LoraRuby.wgs84(4.89, 52.37),
radius: 5000.0,
}

query = <<~CYPHER
MATCH (c:City)
WHERE c.founded >= $since
AND geo.distance(c.location, $near) < $radius
RETURN c.name AS name
CYPHER

plan = db.explain(query, params)
profile = db.profile(query, params)

Structured result handling

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

Available guards: node?, relationship?, path?, point?, temporal? — re-exported on both LoraRuby and LoraRuby::Types.

Typed helpers

db.execute(
"CREATE (:Trip {when: $when, span: $span, origin: $origin})",
{
when: LoraRuby.datetime("2026-05-01T10:15:00Z"),
span: LoraRuby.duration("PT90M"),
origin: LoraRuby.wgs84(4.89, 52.37),
},
)

Available helpers: date, time, localtime, datetime, localdatetime, duration, cartesian, cartesian_3d, wgs84, wgs84_3d.

Handle errors

begin
db.execute("BAD QUERY")
rescue LoraRuby::QueryError => e
puts "query failed: #{e.message}"
rescue LoraRuby::InvalidParamsError => e
puts "bad params: #{e.message}"
end

LoraRuby::Error is the common base class — rescue it if you don't need to distinguish.

Rack / Rails integration

# config/initializers/lora.rb
require "lora_ruby"

LORA_DB = LoraRuby::Database.create

# app/controllers/users_controller.rb
def show
res = LORA_DB.execute(
"MATCH (u:User {id: $id}) RETURN u {.id, .handle, .tier} AS user",
{ id: params[:id].to_i },
)
return head :not_found if res["rows"].empty?
render json: res["rows"].first["user"]
rescue LoraRuby::QueryError => e
render json: { error: e.message }, status: :bad_request
end

Persisting your graph

LoraDB can save the in-memory graph to a single file and restore it later. Ruby has three persistence shapes:

  • LoraRuby::Database.create / LoraRuby::Database.new => in-memory
  • LoraRuby::Database.create("app", {"database_dir": "./data"}) / LoraRuby::Database.new("app", { database_dir: "./data" }) => container-backed
  • LoraRuby::Database.open_wal("./data/wal", snapshot_dir: "./data/snapshots") => explicit WAL with optional managed snapshots
require 'lora_ruby'

db = LoraRuby::Database.new # in-memory
# db = LoraRuby::Database.new("app", { database_dir: "./data" }) # archive: ./data/app.loradb
db.execute("CREATE (:Person {name: 'Ada'})")

# Save everything to disk.
meta = db.save_snapshot("graph.bin")
puts "#{meta['nodeCount']} nodes, #{meta['relationshipCount']} relationships"

# Restore into a fresh handle (in a new process, for example).
db = LoraRuby::Database.new
db.load_snapshot("graph.bin")

durable = LoraRuby::Database.open_wal(
"./data/wal",
snapshot_dir: "./data/snapshots",
snapshot_every_commits: 1000,
snapshot_keep_old: 2,
snapshot_options: {
compression: { format: "gzip", level: 1 },
},
)
durable.close

Both save and load process the whole graph. A crash between saves loses every mutation since the last save unless you opened the database with WAL. See the canonical Snapshots guide for the wire format and atomic-rename guarantees.

Passing a database name and directory opens or creates an container-backed persistent database at <database_dir>/<name>.loradb. Reopening the same path replays committed writes before the handle is returned. open_wal opens a raw WAL directory; when snapshot_dir and snapshot_every_commits are set, the database writes managed checkpoint snapshots after that many committed transactions. Ruby does not expose WAL status, truncate, or sync-mode controls; use Rust or lora-server for those operator knobs. Call db.close before reopening the same archive or WAL directory inside one process.

Common Patterns

Bulk insert from a Ruby array

rows = (1..100).map { |i| { id: i, name: "user-#{i}" } }

db.execute(
"UNWIND $rows AS row CREATE (:User {id: row.id, name: row.name})",
{ rows: rows },
)

See UNWIND.

Other methods

db.clear # drop all nodes + relationships → nil
db.close # release the native handle
db.node_count # Integer
db.relationship_count # Integer
LoraRuby::VERSION # gem version

Error Handling

ClassWhen
LoraRuby::ErrorBase — rescue if you don't need to distinguish
LoraRuby::QueryErrorParse / analyze / execute failure
LoraRuby::InvalidParamsErrorA parameter couldn't be mapped to a LoraValue

Engine-level causes live in Troubleshooting.

Performance / Best Practices

  • GVL release. Database#execute calls rb_thread_call_without_gvl, so other Ruby threads run while a query is in flight. Auto-commit reads can overlap on snapshots; write commits and explicit read-write transactions serialize.
  • Interrupts after current query. The engine has no cancellation hook, so 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.
  • String keys on output. Result Hashes always use string keys, matching the Node, Python, WASM, and Go bindings. Input Hashes accept either symbol or string keys.
  • Parameters, not string concatenation. The only safe way to mix untrusted input into a query.

See also