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:
nil → Null, true/false → Boolean, Integer → Integer,
Float → Float, String/Symbol → String, Array → List,
Hash → Map. 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 queryMutating 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-memoryLoraRuby::Database.create("app", {"database_dir": "./data"})/LoraRuby::Database.new("app", { database_dir: "./data" })=> container-backedLoraRuby::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
| Class | When |
|---|---|
LoraRuby::Error | Base — rescue if you don't need to distinguish |
LoraRuby::QueryError | Parse / analyze / execute failure |
LoraRuby::InvalidParamsError | A parameter couldn't be mapped to a LoraValue |
Engine-level causes live in Troubleshooting.
Performance / Best Practices
- GVL release.
Database#executecallsrb_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
- Ten-Minute Tour — guided walkthrough.
- Queries → Parameters — binding typed values.
- Data Types — Ruby ↔ engine mapping.
- Binding README — the source-of-truth install and build guide.
- Troubleshooting.