Skip to main content

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_ffi static library on disk (built locally with cargo build --release -p lora-ffi, or downloaded from a tagged GitHub Release as lora-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/int64Integer, float64Float, stringString, boolBoolean, nilNull, []anyList, map[string]anyMap. 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-memory
  • lora.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

CodeWhen
LORA_ERRORParse / analyze / execute failure
INVALID_PARAMSA parameter value couldn't be mapped
PANICThe engine panicked; the FFI caught it and surfaced the message
UNKNOWNCatch-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. Parallel Execute calls on the same handle serialise on the engine mutex. For read parallelism, spin up multiple Database instances (each with its own graph).
  • No cancellation. ExecuteContext returns 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