neogo

package module
v1.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 28, 2025 License: MIT Imports: 12 Imported by: 0

README

neogo

logo

Go Report Card codecov Go Reference

A Golang-ORM for Neo4J which creates idiomatic & fluent Cypher.

[!WARNING] The neogo API is still in an experimental phase. Expect minor changes and additions until the first release.

Overview

neogo was designed to make writing Cypher as simple as possible, providing a safety-net and reducing boilerplate by leveraging canonical representations of nodes and relationships as Golang structs.

  • Hands-free un/marshalling between Go and Neo4J
  • No dynamic property, variable, label qualification necessary
  • Creates readable, interoperable Cypher queries
  • Abstract nodes with multiple concrete implementers
  • Heavily tested; full coverage of Neo4J docs examples (see internal/tests)
  • Automatic & explicit:
    • Variable qualification
    • Node/relationship label patterns
    • Parameter injection

Getting Started

See the following resources to get started with neogo:

Example

type Person struct {
	neogo.Node `neo4j:"Person"`

	Name    string `json:"name"`
	Surname string `json:"surname"`
	Age     int    `json:"age"`
}

func main() {
    // Simply obtain an instance of the neo4j.DriverWithContext
    d := neogo.New(driverWithContext)

    person := Person{
        Name:    "Spongebob",
        Surname: "Squarepants",
    }
    // person.GenerateID() can be used
    person.ID = "some-unique-id"

    err := d.Exec().
        Create(db.Node(&person)).
        Set(db.SetPropValue(&person.Age, 20)).
        Return(&person).
        Print().
        Run(ctx)
    // Output:
    // CREATE (person:Person {name: $person_name, surname: $person_surname})
    // SET person.age = $v1
    // RETURN person

    fmt.Printf("person: %v\n", person)
    // Output:
    // person: {{some-unique-id} Spongebob Squarepants 20}
}

Contributions

See the contributing guide for detailed instructions on how to start contibuting to neogo.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExtractNodeLabels

func ExtractNodeLabels(i any) []string

func ExtractRelationshipType

func ExtractRelationshipType(relationship any) string

func NewMock

func NewMock() mockDriver

NewMock creates a mock neogo Driver for testing.

func NewNode

func NewNode[N any, PN interface {
	INode
	internal.IDSetter
	*N
}]() PN

NewNode creates a new node with a random ID.

Example
package main

import (
	"fmt"

	"github.com/rlch/neogo"
)

func main() {
	n := neogo.NewNode[neogo.Node]()
	fmt.Printf("generated: %v", n.ID != "")
}
Output:

generated: true

func NodeWithID

func NodeWithID[N any, PN interface {
	INode
	internal.IDSetter
	*N
}](id string,
) PN

NodeWithID creates a new node with the given ID.

Example
package main

import (
	"fmt"

	"github.com/rlch/neogo"
)

func main() {
	n := neogo.NodeWithID[neogo.Node]("test")
	fmt.Printf("id: %v", n.ID)
}
Output:

id: test

func WithSessionConfig

func WithSessionConfig(configurers ...func(*neo4j.SessionConfig)) func(ec *execConfig)

WithSessionConfig configures the session used by Exec().

func WithTxConfig

func WithTxConfig(configurers ...func(*neo4j.TransactionConfig)) func(ec *execConfig)

WithTxConfig configures the transaction used by Exec().

func WithTypes

func WithTypes(types ...any) func(*driver)

WithTypes is an option for New that allows you to register instances of IAbstract, INode and IRelationship to be used with neogo.

Types

type Abstract

type Abstract = internal.Abstract

Abstract is a base type for all abstract nodes. An abstract node can have multiple concrete implementers, where each implementer must have a distinct label. This means that each node will have at least 2 labels.

A useful design pattern for constructing abstract nodes is to create a base type which provides an implementation for IAbstract and embed Abstract + Node, then embed that type in all concrete implementers:

type Organism interface {
	internal.IAbstract
}

type BaseOrganism struct {
	internal.Abstract `neo4j:"Organism"`
	internal.Node

	Alive bool `json:"alive"`
}

func (b BaseOrganism) Implementers() []internal.IAbstract {
	return []internal.IAbstract{
		&Human{},
		&Dog{},
	}
}

type Human struct {
	BaseOrganism `neo4j:"Human"`
	Name         string `json:"name"`
}

type Dog struct {
	BaseOrganism `neo4j:"Dog"`
	Borfs        bool `json:"borfs"`
}

type Config

type Config func(*driver)

func WithCausalConsistency

func WithCausalConsistency(when func(ctx context.Context) string) Config

type Driver

type Driver interface {
	// DB returns the underlying neo4j driver.
	DB() neo4j.DriverWithContext

	// ReadSession creates a new read-access session based on the specified session configuration.
	ReadSession(ctx context.Context, configurers ...func(*neo4j.SessionConfig)) readSession

	// WriteSession creates a new write-access session based on the specified session configuration.
	WriteSession(ctx context.Context, configurers ...func(*neo4j.SessionConfig)) writeSession

	// Exec creates a new transaction + session and executes the given Cypher
	// query.
	//
	// The access mode is inferred from the clauses used in the query. If using
	// Cypher() to inject a write query, one should use [WithSessionConfig] to
	// override the access mode.
	//
	// The session is closed after the query is executed.
	Exec(configurers ...func(*execConfig)) Query
}

Driver represents a pool of connections to a neo4j server or cluster. It provides an entrypoint to a neogo query.Client, which can be used to build cypher queries.

It's safe for concurrent use.

Example
ctx := context.Background()
var d Driver
if testing.Short() {
	m := NewMock()
	m.Bind(map[string]any{
		"person": Person{
			Node:    internal.Node{ID: "some-unique-id"},
			Name:    "Spongebob",
			Surname: "Squarepants",
			Age:     20,
		},
	})
	d = m
} else {
	neo4j, cancel := startNeo4J(ctx)
	d = New(neo4j)
	defer func() {
		if err := cancel(ctx); err != nil {
			panic(err)
		}
	}()
}

person := Person{
	Name:    "Spongebob",
	Surname: "Squarepants",
}
person.ID = "some-unique-id"
err := d.Exec().
	Create(db.Node(&person)).
	Set(db.SetPropValue(&person.Age, 20)).
	Return(&person).
	Print().
	Run(ctx)
fmt.Printf("err: %v\n", err)
fmt.Printf("person: %v\n", person)
Output:

CREATE (person:Person {id: $person_id, name: $person_name, surname: $person_surname})
SET person.age = $v1
RETURN person
err: <nil>
person: {{some-unique-id} Spongebob Squarepants 20}
Example (ReadSession)
ctx := context.Background()
var d Driver

if testing.Short() {
	m := NewMock()
	records := make([]map[string]any, 11)
	for i := range records {
		records[i] = map[string]any{"i": i}
	}
	m.BindRecords(records)
	records2x := make([]map[string]any, 11)
	for i := range records2x {
		records2x[i] = map[string]any{"i * 2": i * 2}
	}
	m.BindRecords(records2x)
	d = m
} else {
	neo4j, cancel := startNeo4J(ctx)
	d = New(neo4j)
	defer func() {
		if err := cancel(ctx); err != nil {
			panic(err)
		}
	}()
}

var ns, nsTimes2 []int
session := d.ReadSession(ctx)
defer func() {
	if err := session.Close(ctx); err != nil {
		panic(err)
	}
}()
err := session.ReadTransaction(ctx, func(begin func() Query) error {
	if err := begin().
		Unwind("range(0, 10)", "i").
		Return(db.Qual(&ns, "i")).Run(ctx); err != nil {
		return err
	}
	if err := begin().
		Unwind(&ns, "i").
		Return(db.Qual(&nsTimes2, "i * 2")).Run(ctx); err != nil {
		return err
	}
	return nil
})
fmt.Printf("err: %v\n", err)

fmt.Printf("ns:       %v\n", ns)
fmt.Printf("nsTimes2: %v\n", nsTimes2)
Output:

err: <nil>
ns:       [0 1 2 3 4 5 6 7 8 9 10]
nsTimes2: [0 2 4 6 8 10 12 14 16 18 20]
Example (RunWithParams)
ctx := context.Background()
var d Driver
if testing.Short() {
	m := NewMock()
	m.Bind(map[string]any{
		"$ns": []int{1, 2, 3},
	})
	d = m
} else {
	neo4j, cancel := startNeo4J(ctx)
	d = New(neo4j)
	defer func() {
		if err := cancel(ctx); err != nil {
			panic(err)
		}
	}()
}

var ns []int
err := d.Exec().
	Return(db.Qual(&ns, "$ns")).
	RunWithParams(ctx, map[string]interface{}{
		"ns": []int{1, 2, 3},
	})

fmt.Printf("err: %v\n", err)
fmt.Printf("ns: %v\n", ns)
Output:

err: <nil>
ns: [1 2 3]
Example (StreamWithParams)
ctx := context.Background()
var d Driver
n := 3

if testing.Short() {
	m := NewMock()
	records := make([]map[string]any, n+1)
	for i := range records {
		records[i] = map[string]any{"i": i}
	}
	m.BindRecords(records)
	d = m
} else {
	neo4j, cancel := startNeo4J(ctx)
	d = New(neo4j)
	defer func() {
		if err := cancel(ctx); err != nil {
			panic(err)
		}
	}()
}

ns := []int{}
session := d.ReadSession(ctx)
defer func() {
	if err := session.Close(ctx); err != nil {
		panic(err)
	}
}()
err := session.ReadTransaction(ctx, func(begin func() Query) error {
	var num int
	params := map[string]interface{}{
		"total": n,
	}
	return begin().
		Unwind("range(0, $total)", "i").
		Return(db.Qual(&num, "i")).
		StreamWithParams(ctx, params, func(r query.Result) error {
			for i := 0; r.Next(ctx); i++ {
				if err := r.Read(); err != nil {
					return err
				}
				ns = append(ns, num)
			}
			return nil
		})
})

fmt.Printf("err: %v\n", err)
fmt.Printf("ns: %v\n", ns)
Output:

err: <nil>
ns: [0 1 2 3]
Example (WriteSession)
ctx := context.Background()
var d Driver
if testing.Short() {
	m := NewMock()
	m.Bind(nil)
	records := make([]map[string]any, 10)
	for i := range records {
		records[i] = map[string]any{"p": &Person{
			Node: internal.Node{
				ID: strconv.Itoa(i + 1),
			},
		}}
	}
	m.BindRecords(records)
	d = m
} else {
	neo4j, cancel := startNeo4J(ctx)
	d = New(neo4j)
	defer func() {
		if err := cancel(ctx); err != nil {
			panic(err)
		}
	}()
}

var people []*Person
session := d.WriteSession(ctx)
defer func() {
	if err := session.Close(ctx); err != nil {
		panic(err)
	}
}()
err := session.WriteTransaction(ctx, func(begin func() Query) error {
	if err := begin().
		Unwind("range(1, 10)", "i").
		Merge(db.Node(
			db.Qual(
				Person{},
				"p",
				db.Props{"id": "toString(i)"},
			),
		)).
		Run(ctx); err != nil {
		return err
	}
	if err := begin().
		Unwind("range(1, 10)", "i").
		Match(db.Node(db.Qual(&people, "p"))).
		Where(db.And(
			db.Cond("p.id", "=", "toString(i)"),
		)).
		Return(&people).
		Run(ctx); err != nil {
		return err
	}
	return nil
})
ids := make([]string, len(people))
for i, p := range people {
	ids[i] = p.ID
}
fmt.Printf("err: %v\n", err)
fmt.Printf("ids: %v\n", ids)
Output:

err: <nil>
ids: [1 2 3 4 5 6 7 8 9 10]

func New

func New(neo4j neo4j.DriverWithContext, configurers ...Config) Driver

New creates a new neogo Driver from a neo4j.DriverWithContext.

type Expression

type Expression = query.Expression

Expression is an interface for compiling a Cypher expression outside the context of a query.

type IAbstract

type IAbstract = internal.IAbstract

IAbstract is an interface for abstract nodes. See Abstract for the default implementation.

type INode

type INode = internal.INode

INode is an interface for nodes. See Node for the default implementation.

type IRelationship

type IRelationship = internal.IRelationship

IRelationship is an interface for relationships. See Relationship for the default implementation.

type Label added in v1.0.2

type Label = internal.Label

Label is a used to specify a label for a node. This allows for multiple labels to be specified idiomatically.

type Robot struct {
	neogo.Label `neo4j:"Robot"`
}

type Node

type Node = internal.Node

Node is a base type for all nodes.

The neo4j tag is used to specify the label for the node. Multiple labels may be specified idiomatically by nested Node types. See internal/tests for examples.

type Person struct {
 neogo.Node `neo4j:"Person"`

 Name string `json:"name"`
 Age  int    `json:"age"`
}

type Query

type Query = query.Query

Query is the interface for constructing a Cypher query.

type Relationship

type Relationship = internal.Relationship

Relationship is a base type for all relationships.

The neo4j tag is used to specify the type for the relationship.

type ActedIn struct {
	neogo.Relationship `neo4j:"ACTED_IN"`

	Role string `json:"role"`
}

type Transaction

type Transaction interface {
	// Run executes a statement on this transaction and returns a result
	// Contexts terminating too early negatively affect connection pooling and degrade the driver performance.
	Run(work Work) error
	// Commit commits the transaction
	// Contexts terminating too early negatively affect connection pooling and degrade the driver performance.
	Commit(ctx context.Context) error
	// Rollback rolls back the transaction
	// Contexts terminating too early negatively affect connection pooling and degrade the driver performance.
	Rollback(ctx context.Context) error
	// Close rolls back the actual transaction if it's not already committed/rolled back
	// and closes all resources associated with this transaction
	// Contexts terminating too early negatively affect connection pooling and degrade the driver performance.
	Close(ctx context.Context, joinedErrors ...error) error
}

Transaction represents an explicit transaction that can be committed or rolled back.

type Valuer

type Valuer[V neo4j.RecordValue] interface {
	Marshal() (*V, error)
	Unmarshal(*V) error
}

Valuer allows arbitrary types to be marshalled into and unmarshalled from Neo4J data types. This allows any type (as oppposed to stdlib types, INode, IAbstract, IRelationship, and structs with json tags) to be used with neogo. The valid Neo4J data types are defined by neo4j.RecordValue.

For example, here we define a custom type MyString that marshals to and from a string, one of the types in the neo4j.RecordValue union:

type MyString string

var _ Valuer[string] = (*MyString)(nil)

func (s MyString) Marshal() (*string, error) {
	return func(s string) *string {
		return &s
	}(string(s)), nil
}

func (s *MyString) Unmarshal(v *string) error {
	*s = MyString(*v)
	return nil
}

type Work

type Work func(start func() Query) error

Work is a function that allows Cypher to be executed within a Transaction.

Directories

Path Synopsis
Package db provides building blocks for constructing Cypher clauses.
Package db provides building blocks for constructing Cypher clauses.
Package query provides client interfaces for constructing and executing Cypher queries.
Package query provides client interfaces for constructing and executing Cypher queries.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL