Skip to content
gr

Quick start

From an empty terminal to a persisted, queryable graph database in five minutes.

This guide walks you from nothing to a real graph database with nodes, relationships, an index, and a persisted .gr file you can reopen later.

With the Go library

Create a file main.go:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tamnd/gr"
)

func main() {
	ctx := context.Background()

	db, err := gr.Open("social.gr", gr.Options{})
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Create an index so name lookups are fast.
	_, err = db.Exec(ctx, `CREATE INDEX FOR (p:Person) ON (p.name)`, nil)
	if err != nil {
		log.Fatal(err)
	}

	// Add two people.
	_, err = db.Exec(ctx, `
		CREATE (:Person {name: $a, age: 30})
		CREATE (:Person {name: $b, age: 25})
	`, map[string]any{"a": "Alice", "b": "Bob"})
	if err != nil {
		log.Fatal(err)
	}

	// Connect them.
	_, err = db.Exec(ctx, `
		MATCH (a:Person {name: $a}), (b:Person {name: $b})
		CREATE (a)-[:KNOWS {since: 2022}]->(b)
	`, map[string]any{"a": "Alice", "b": "Bob"})
	if err != nil {
		log.Fatal(err)
	}

	// Query the graph.
	res, err := db.Query(ctx, `
		MATCH (a:Person)-[r:KNOWS]->(b:Person)
		RETURN a.name AS from, b.name AS to, r.since AS since
	`, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Close()

	for res.Next() {
		rec := res.Record()
		fmt.Printf("%v knows %v (since %v)\n",
			rec.Get("from"), rec.Get("to"), rec.Get("since"))
	}
	if err := res.Err(); err != nil {
		log.Fatal(err)
	}
}

Run it:

go mod init example && go get github.com/tamnd/gr@latest && go run main.go
Alice knows Bob (since 2022)

The file social.gr now exists on disk. Run the program again and it picks up where it left off — the nodes, relationships, and index are all there.

With the CLI

Create the same graph interactively:

gr shell social.gr
gr> CREATE INDEX FOR (p:Person) ON (p.name);
Created 1 index.
gr> CREATE (:Person {name:"Alice", age:30}), (:Person {name:"Bob", age:25});
Created 2 nodes, set 4 properties.
gr> MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"}) CREATE (a)-[:KNOWS {since:2022}]->(b);
Created 1 relationship, set 1 property.
gr> MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a.name, b.name, r.since;
╔══════════╦══════════╦═══════╗
║ a.name   ║ b.name   ║ since ║
╠══════════╬══════════╬═══════╣
║ Alice    ║ Bob      ║ 2022  ║
╚══════════╩══════════╩═══════╝
gr> :quit

One-shot mode without entering the shell:

gr run social.gr "MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age"
╔══════════╦═══════╗
║ n.name   ║ n.age ║
╠══════════╬═══════╣
║ Bob      ║ 25    ║
║ Alice    ║ 30    ║
╚══════════╩═══════╝

JSON output:

gr run --format json social.gr "MATCH (n:Person) RETURN n.name"
[{"n.name":"Alice"},{"n.name":"Bob"}]

What was persisted

After either path, social.gr holds the complete graph: the nodes, the relationship, the index, and all properties. Two sidecar files (social.gr-wal, social.gr-shm) appear while the database is open and disappear on a clean close, exactly like SQLite's WAL files. Include only social.gr in a backup; the sidecars are ephemeral.

Schema inspection

gr run social.gr "SHOW INDEXES"
gr run social.gr "SHOW LABELS"
gr run social.gr "SHOW RELATIONSHIP TYPES"

Or inside the shell:

gr> :schema
Labels    : Person
Rel types : KNOWS
Indexes   : INDEX FOR (p:Person) ON (p.name)
Constraints: (none)

Next steps