Using LoraDB in Go
Overview
lora-go is a thin cgo wrapper over the shared lora-ffi
C ABI. The engine runs in-process — no separate server, no socket
hop. Values follow the same tagged model as the Node, Python, WASM,
and Ruby bindings (primitives pass through; nodes, relationships,
paths, temporals, and points come back as map[string]any with a
"kind" discriminator).
Installation / Setup
Requirements
- Go 1.21+
- A C toolchain with cgo enabled (
clang/gcc) - The
liblora_ffistatic library on disk (built locally withcargo build --release -p lora-ffi, or downloaded from a tagged GitHub Release aslora-ffi-vX.Y.Z-<triple>.tar.gz)
Install
go get github.com/lora-db/lora/crates/lora-go
Because the binding links against the Rust engine, go build needs
liblora_ffi.a on disk before it runs. The simplest path is to
clone the workspace and build the FFI in-tree:
git clone https://github.com/lora-db/lora
cd lora
cargo build --release -p lora-ffi # produces target/release/liblora_ffi.a
cd crates/lora-go
go test -race ./...
The default #cgo LDFLAGS in lora.go resolves to
${SRCDIR}/../../target/release/liblora_ffi.a — the right path in
the workspace layout.
For consumer projects outside the repo, build lora-ffi once and
override the cgo flags in the environment:
export CGO_CFLAGS="-I$PWD/lora/crates/lora-go/include"
export CGO_LDFLAGS="-L$PWD/lora/target/release -llora_ffi -lm -ldl -lpthread"
go build ./...
See crates/lora-go/README.md
for the full build-from-release-archive flow.
Creating a Client / Connection
import lora "github.com/lora-db/lora/crates/lora-go"
db, err := lora.New()
if err != nil { log.Fatal(err) }
defer db.Close()
lora.New() and lora.NewDatabase() are the same constructor —
both return a ready-to-use handle over an empty in-memory graph.
Running Your First Query
package main
import (
"fmt"
"log"
lora "github.com/lora-db/lora/crates/lora-go"
)
func main() {
db, err := lora.New()
if err != nil { log.Fatal(err) }
defer db.Close()
if _, err := db.Execute(
"CREATE (:Person {name: 'Ada', born: 1815})",
nil,
); err != nil { log.Fatal(err) }
r, err := db.Execute(
"MATCH (p:Person) RETURN p.name AS name, p.born AS born",
nil,
)
if err != nil { log.Fatal(err) }
fmt.Println(r.Columns, r.Rows)
// [name born] [map[name:Ada born:1815]]
}
Examples
Parameterised query
r, err := db.Execute(
"MATCH (p:Person) WHERE p.name = $name RETURN p.name AS name",
lora.Params{"name": "Ada"},
)
Go values map automatically: int/int64 → Integer,
float64 → Float, string → String, bool → Boolean,
nil → Null, []any → List, map[string]any → Map. Use the
tagged helpers for dates, durations, and points — see
typed helpers below.
Structured result handling
r, err := db.Execute("MATCH (n:Person) RETURN n", nil)
if err != nil { log.Fatal(err) }
for _, row := range r.Rows {
if lora.IsNode(row["n"]) {
n := row["n"].(map[string]any)
fmt.Println(n["id"], n["labels"], n["properties"])
}
}
Available guards: IsNode, IsRelationship, IsPath, IsPoint,
IsTemporal.
Context cancellation (important caveat)
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
r, err := db.ExecuteContext(ctx, "MATCH (n) RETURN count(n)", nil)
ExecuteContext honours context.Context deadlines on the Go side
— the call returns ctx.Err() as soon as the context fires. But
the engine does not yet support mid-query cancellation, so the
native call keeps running in a helper goroutine and holds the
database's internal mutex until it finishes. Any follow-up call
that needs the mutex blocks until then.
If you rely on a hard deadline, either keep queries small enough that their worst-case latency is acceptable even if they can't be interrupted, or guard the database with a higher-level rate-limiter.
Typed helpers
db.Execute(
"CREATE (:Trip {when: $when, span: $span, origin: $origin})",
lora.Params{
"when": lora.DateTime("2026-05-01T10:15:00Z"),
"span": lora.Duration("PT90M"),
"origin": lora.WGS84(4.89, 52.37),
},
)
Available helpers: Date, Time, LocalTime, DateTime,
LocalDateTime, Duration, Cartesian, Cartesian3D, WGS84,
WGS84_3D.
Handle errors
if err != nil {
var lerr *lora.LoraError
if errors.As(err, &lerr) {
switch lerr.Code {
case lora.CodeInvalidParams:
// bad params
case lora.CodeLoraError:
// parse / analyze / execute failure
}
}
}
Persisting your graph
LoraDB can save the in-memory graph to a single file and restore it later. Go now supports the same simple initialization rule as the other filesystem-backed bindings:
lora.New()/lora.NewDatabase()=> in-memorylora.New("app", lora.Options{DatabaseDir: "./data"})/lora.NewDatabase("app", lora.Options{DatabaseDir: "./data"})=> persistent
import lora "github.com/lora-db/lora/crates/lora-go"
db, err := lora.New() // in-memory
// db, err := lora.New("app", lora.Options{DatabaseDir: "./data"}) // persistent: ./data/app.loradb
if err != nil { log.Fatal(err) }
defer db.Close()
if _, err := db.Execute("CREATE (:Person {name: 'Ada'})", nil); err != nil {
log.Fatal(err)
}
meta, err := db.SaveSnapshot("graph.bin")
if err != nil { log.Fatal(err) }
fmt.Printf("nodes=%d rels=%d\n", meta.NodeCount, meta.RelationshipCount)
db2, err := lora.New()
if err != nil { log.Fatal(err) }
defer db2.Close()
if _, err := db2.LoadSnapshot("graph.bin"); err != nil {
log.Fatal(err)
}
SnapshotMeta.WalLsn is a *uint64; it is nil for a pure snapshot
and non-nil when you load a checkpoint snapshot written by a
WAL-enabled Rust or lora-server deployment. Both save and load hold
the store mutex for the duration of the call — concurrent
Execute calls block until the snapshot operation finishes. A crash
between saves loses every mutation since the last save.
Passing a database name and directory opens or creates an archive-backed persistent
database at <databaseDir>/<name>.loradb. Reopening the same path replays committed
writes before the handle is returned. This first Go persistence slice
intentionally stays small: the binding exposes archive-backed
initialization plus snapshots, but not checkpoint, truncate, status, or
sync-mode controls.
If you run lora-server alongside a Go client, you can also drive the
admin surface as an ordinary HTTP request — see
lora-server → Snapshots, WAL, and restore
and POST /admin/snapshot/save.
See the canonical Snapshots guide for the full metadata shape, atomic-rename guarantees, and boundaries, and WAL and checkpoints for the recovery model.
Common Patterns
Bulk insert from a Go slice
rows := make([]any, 0, 100)
for i := 0; i < 100; i++ {
rows = append(rows, map[string]any{"id": i, "name": fmt.Sprintf("user-%d", i)})
}
db.Execute(
"UNWIND $rows AS row CREATE (:User {id: row.id, name: row.name})",
lora.Params{"rows": rows},
)
See UNWIND.
Other methods
db.Clear() // drop all nodes + relationships
db.NodeCount() // int64, error
db.RelationshipCount() // int64, error
db.Version() // module / engine version string
Error Handling
| Code | When |
|---|---|
LORA_ERROR | Parse / analyze / execute failure |
INVALID_PARAMS | A parameter value couldn't be mapped |
PANIC | The engine panicked; the FFI caught it and surfaced the message |
UNKNOWN | Catch-all for messages without a recognised prefix |
Engine-level causes live in Troubleshooting.
Performance / Best Practices
- Platform support. Linux and macOS (x86_64, arm64). Windows is not yet supported — revisit once a Windows Go target ships.
- One mutex per
Database. ParallelExecutecalls on the same handle serialise on the engine mutex. For read parallelism, spin up multipleDatabaseinstances (each with its own graph). - No cancellation.
ExecuteContextreturns the context error immediately but the native call keeps running. See the caveat above. - 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 — Go ↔ engine mapping.
- Binding README — the source-of-truth install and build guide.
- Troubleshooting.