Embedded Library Guide¶
Every operation that can do unbounded work takes a context.Context as its
first argument. Pass a real one: a query or traversal that cannot be cancelled
runs to completion holding locks, long after whoever asked for it has gone.
context.Background() is fine for short-lived scripts.
Crystal can be used as an embedded graph database in any Go application, similar to how SQLite is used for relational data.
Installation¶
Opening a Database¶
package main
import "crystal/pkg/crystal"
func main() {
db, err := crystal.Open(crystal.Options{
StorageType: "disk",
DataDir: "./data",
SyncOnWrite: false,
})
if err != nil {
panic(err)
}
defer db.Close()
}
Storage Types¶
| Type | Description |
|---|---|
disk |
Persistent JSON file storage (default) |
memory |
In-memory only, data lost on close |
Options¶
| Field | Type | Default | Description |
|---|---|---|---|
| StorageType | string | "disk" |
Storage backend |
| DataDir | string | "./data" |
Directory for data files |
| SyncOnWrite | bool | false |
Full snapshot flush on every write (rarely needed) |
| FlushInterval | time.Duration | 0 |
Auto-checkpoint interval (0 = disabled) |
CRUD Operations¶
Vertices¶
v, err := db.AddVertex(ctx, "Person", crystal.Properties{
"name": "Alice",
"age": 30,
})
v, err := db.GetVertex(ctx, id)
v.Properties["age"] = 31
err := db.UpdateVertex(ctx, v)
err := db.DeleteVertex(ctx, id)
Edges¶
e, err := db.AddEdge(ctx, sourceID, targetID, "knows", crystal.Properties{
"since": "2020",
})
e, err := db.GetEdge(ctx, id)
e.Weight = 2.5
err := db.UpdateEdge(ctx, e)
err := db.DeleteEdge(ctx, id)
Listing¶
Neighbors¶
neighbors, err := db.Neighbors(ctx, vertexID, crystal.DirectionOut)
neighbors, err := db.Neighbors(ctx, vertexID, crystal.DirectionIn)
neighbors, err := db.Neighbors(ctx, vertexID, crystal.DirectionBoth)
Atomic Batches¶
batch := db.Batch().
PutVertex(person).
PutVertex(server).
PutEdge(accessed)
if err := db.Apply(ctx, batch); err != nil {
// Nothing was applied.
}
All of the operations take effect or none do, across a crash as well as within the process. Later operations see earlier ones, so a batch can create a vertex and attach an edge to it in the same unit.
This is an atomic batch, not an interactive transaction: there is no Begin/read/decide/Commit cycle, because reading your own uncommitted writes needs snapshot isolation that Crystal does not yet have.
Consistent Reads¶
err := db.View(ctx, func(view crystal.Reader) error {
edges, err := view.GetEdgesByVertex(ctx, id, crystal.DirectionOut)
if err != nil {
return err
}
for _, e := range edges {
// Guaranteed to exist: the same view served both reads.
v, err := view.GetVertex(ctx, e.TargetID)
...
}
return nil
})
Every read inside the callback sees the same state of the graph. Without it, two reads can straddle a write and produce a graph that never existed.
Do not write from inside a view — it holds the read lock for its duration.
Queries¶
The query builder provides a fluent API for filtering and paginating results.
Vertex Queries¶
q := db.QueryVertices().
OfType("Person").
WithLabel("suspect").
PropertyEquals("city", "Berlin").
Limit(10).
Offset(0)
result, err := db.Execute(ctx, q)
for _, v := range result.Vertices {
fmt.Println(v.Properties["name"])
}
Custom Filters¶
q := db.QueryVertices().
OfType("Person").
Where(func(props crystal.Properties) bool {
age, ok := props.GetInt("age")
return ok && age > 25
})
result, err := db.Execute(ctx, q)
Edge Queries¶
Neighbor Queries¶
q := db.QueryFrom(vertexID).
Out().
OfType("Server").
Depth(2).
Limit(50)
result, err := db.Execute(ctx, q)
Traversal¶
BFS¶
err := db.TraverseBFS(ctx, startID, 3, func(v *crystal.Vertex, depth int) bool {
fmt.Printf("depth %d: %s (%s)\n", depth, v.ID, v.Type)
return true // return false to stop
})
DFS¶
err := db.TraverseDFS(ctx, startID, 5, func(v *crystal.Vertex, depth int) bool {
fmt.Printf("found: %s\n", v.Properties["name"])
return true
})
Shortest Path¶
path, err := db.ShortestPath(ctx, fromID, toID)
if err != nil {
fmt.Println("no path found")
return
}
fmt.Printf("path length: %d edges\n", len(path.Edges))
for _, v := range path.Vertices {
fmt.Println(v.ID, v.Type)
}
Statistics¶
Persistence and Durability¶
The disk backend uses a Write-Ahead Log (WAL). Every mutation is appended to the WAL and fsynced before being applied in memory, so acknowledged writes survive crashes.
Set FlushInterval to enable automatic checkpointing (snapshot + WAL truncation):
db, err := crystal.Open(crystal.Options{
StorageType: "disk",
DataDir: "./data",
FlushInterval: 30 * time.Second,
})
You can also trigger a manual checkpoint at any time:
Close() automatically stops the checkpointer and writes a final snapshot.
Investigation Mapping Example¶
db, _ := crystal.Open(crystal.DefaultOptions())
defer db.Close()
alice, _ := db.AddVertex(ctx, "Person", crystal.Properties{"name": "Alice"})
bob, _ := db.AddVertex(ctx, "Person", crystal.Properties{"name": "Bob"})
server, _ := db.AddVertex("Server", crystal.Properties{"ip": "10.0.0.1"})
domain, _ := db.AddVertex("Domain", crystal.Properties{"fqdn": "example.com"})
db.AddEdge(alice.ID, server.ID, "accessed", crystal.Properties{"port": 443})
db.AddEdge(bob.ID, server.ID, "accessed", crystal.Properties{"port": 22})
db.AddEdge(domain.ID, server.ID, "resolves_to", nil)
q := db.QueryFrom(server.ID).In().OfType("Person").Depth(1)
result, _ := db.Execute(ctx, q)
for _, v := range result.Vertices {
fmt.Println("accessed by:", v.Properties["name"])
}
Dialogue Tree Example¶
db, _ := crystal.Open(crystal.Options{StorageType: "memory"})
defer db.Close()
start, _ := db.AddVertex("DialogueNode", crystal.Properties{
"speaker": "NPC",
"text": "Hello traveler, what brings you here?",
})
opt1, _ := db.AddVertex("DialogueNode", crystal.Properties{
"speaker": "Player",
"text": "I'm looking for the ancient temple.",
})
opt2, _ := db.AddVertex("DialogueNode", crystal.Properties{
"speaker": "Player",
"text": "Just passing through.",
})
resp1, _ := db.AddVertex("DialogueNode", crystal.Properties{
"speaker": "NPC",
"text": "Ah, the temple lies to the north.",
})
db.AddEdge(start.ID, opt1.ID, "choice", crystal.Properties{"order": 1})
db.AddEdge(start.ID, opt2.ID, "choice", crystal.Properties{"order": 2})
db.AddEdge(opt1.ID, resp1.ID, "leads_to", nil)
choices, _ := db.Neighbors(start.ID, crystal.DirectionOut)
for _, c := range choices {
fmt.Println("->", c.Properties["text"])
}