Move utility package 'graphdb' to pkg/graphdb
This commit is contained in:
parent
81b755db9b
commit
ced35db5c1
8 changed files with 1130 additions and 0 deletions
1
graphdb/MAINTAINERS
Normal file
1
graphdb/MAINTAINERS
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Michael Crosby <michael@crosbymichael.com> (@crosbymichael)
|
5
graphdb/conn_darwin.go
Normal file
5
graphdb/conn_darwin.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
func NewSqliteConn(root string) (*Database, error) {
|
||||||
|
panic("Not implemented")
|
||||||
|
}
|
23
graphdb/conn_linux.go
Normal file
23
graphdb/conn_linux.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "code.google.com/p/gosqlite/sqlite3" // registers sqlite
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSqliteConn(root string) (*Database, error) {
|
||||||
|
initDatabase := false
|
||||||
|
if _, err := os.Stat(root); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
initDatabase = true
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn, err := sql.Open("sqlite3", root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewDatabase(conn, initDatabase)
|
||||||
|
}
|
473
graphdb/graphdb.go
Normal file
473
graphdb/graphdb.go
Normal file
|
@ -0,0 +1,473 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
createEntityTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS entity (
|
||||||
|
id text NOT NULL PRIMARY KEY
|
||||||
|
);`
|
||||||
|
|
||||||
|
createEdgeTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS edge (
|
||||||
|
"entity_id" text NOT NULL,
|
||||||
|
"parent_id" text NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
CONSTRAINT "parent_fk" FOREIGN KEY ("parent_id") REFERENCES "entity" ("id"),
|
||||||
|
CONSTRAINT "entity_fk" FOREIGN KEY ("entity_id") REFERENCES "entity" ("id")
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
createEdgeIndices = `
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "name_parent_ix" ON "edge" (parent_id, name);
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Entity with a unique id
|
||||||
|
type Entity struct {
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Edge connects two entities together
|
||||||
|
type Edge struct {
|
||||||
|
EntityID string
|
||||||
|
Name string
|
||||||
|
ParentID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entities map[string]*Entity
|
||||||
|
type Edges []*Edge
|
||||||
|
|
||||||
|
type WalkFunc func(fullPath string, entity *Entity) error
|
||||||
|
|
||||||
|
// Graph database for storing entities and their relationships
|
||||||
|
type Database struct {
|
||||||
|
conn *sql.DB
|
||||||
|
mux sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new graph database initialized with a root entity
|
||||||
|
func NewDatabase(conn *sql.DB, init bool) (*Database, error) {
|
||||||
|
if conn == nil {
|
||||||
|
return nil, fmt.Errorf("Database connection cannot be nil")
|
||||||
|
}
|
||||||
|
db := &Database{conn: conn}
|
||||||
|
|
||||||
|
if init {
|
||||||
|
if _, err := conn.Exec(createEntityTable); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := conn.Exec(createEdgeTable); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := conn.Exec(createEdgeIndices); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback := func() {
|
||||||
|
conn.Exec("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create root entities
|
||||||
|
if _, err := conn.Exec("BEGIN"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := conn.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil {
|
||||||
|
rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil {
|
||||||
|
rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Exec("COMMIT"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the underlying connection to the database
|
||||||
|
func (db *Database) Close() error {
|
||||||
|
return db.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the entity id for a given path
|
||||||
|
func (db *Database) Set(fullPath, id string) (*Entity, error) {
|
||||||
|
db.mux.Lock()
|
||||||
|
defer db.mux.Unlock()
|
||||||
|
|
||||||
|
rollback := func() {
|
||||||
|
db.conn.Exec("ROLLBACK")
|
||||||
|
}
|
||||||
|
if _, err := db.conn.Exec("BEGIN EXCLUSIVE"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var entityId string
|
||||||
|
if err := db.conn.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityId); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
if _, err := db.conn.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil {
|
||||||
|
rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e := &Entity{id}
|
||||||
|
|
||||||
|
parentPath, name := splitPath(fullPath)
|
||||||
|
if err := db.setEdge(parentPath, name, e); err != nil {
|
||||||
|
rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.conn.Exec("COMMIT"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if a name already exists in the database
|
||||||
|
func (db *Database) Exists(name string) bool {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
e, err := db.get(name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) setEdge(parentPath, name string, e *Entity) error {
|
||||||
|
parent, err := db.get(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if parent.id == e.id {
|
||||||
|
return fmt.Errorf("Cannot set self as child")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.conn.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the root "/" entity for the database
|
||||||
|
func (db *Database) RootEntity() *Entity {
|
||||||
|
return &Entity{
|
||||||
|
id: "0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the entity for a given path
|
||||||
|
func (db *Database) Get(name string) *Entity {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
e, err := db.get(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) get(name string) (*Entity, error) {
|
||||||
|
e := db.RootEntity()
|
||||||
|
// We always know the root name so return it if
|
||||||
|
// it is requested
|
||||||
|
if name == "/" {
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := split(name)
|
||||||
|
for i := 1; i < len(parts); i++ {
|
||||||
|
p := parts[i]
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
next := db.child(e, p)
|
||||||
|
if next == nil {
|
||||||
|
return nil, fmt.Errorf("Cannot find child for %s", name)
|
||||||
|
}
|
||||||
|
e = next
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all entities by from the name
|
||||||
|
// The key will be the full path of the entity
|
||||||
|
func (db *Database) List(name string, depth int) Entities {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
out := Entities{}
|
||||||
|
e, err := db.get(name)
|
||||||
|
if err != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
children, err := db.children(e, name, depth, nil)
|
||||||
|
if err != nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range children {
|
||||||
|
out[c.FullPath] = c.Entity
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through the child graph of an entity, calling walkFunc for each child entity.
|
||||||
|
// It is safe for walkFunc to call graph functions.
|
||||||
|
func (db *Database) Walk(name string, walkFunc WalkFunc, depth int) error {
|
||||||
|
children, err := db.Children(name, depth)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: the database lock must not be held while calling walkFunc
|
||||||
|
for _, c := range children {
|
||||||
|
if err := walkFunc(c.FullPath, c.Entity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the children of the specified entity
|
||||||
|
func (db *Database) Children(name string, depth int) ([]WalkMeta, error) {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
e, err := db.get(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.children(e, name, depth, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the refrence count for a specified id
|
||||||
|
func (db *Database) Refs(id string) int {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := db.conn.QueryRow("SELECT COUNT(*) FROM edge WHERE entity_id = ?;", id).Scan(&count); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all the id's path references
|
||||||
|
func (db *Database) RefPaths(id string) Edges {
|
||||||
|
db.mux.RLock()
|
||||||
|
defer db.mux.RUnlock()
|
||||||
|
|
||||||
|
refs := Edges{}
|
||||||
|
|
||||||
|
rows, err := db.conn.Query("SELECT name, parent_id FROM edge WHERE entity_id = ?;", id)
|
||||||
|
if err != nil {
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
var parentId string
|
||||||
|
if err := rows.Scan(&name, &parentId); err != nil {
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
refs = append(refs, &Edge{
|
||||||
|
EntityID: id,
|
||||||
|
Name: name,
|
||||||
|
ParentID: parentId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the reference to an entity at a given path
|
||||||
|
func (db *Database) Delete(name string) error {
|
||||||
|
db.mux.Lock()
|
||||||
|
defer db.mux.Unlock()
|
||||||
|
|
||||||
|
if name == "/" {
|
||||||
|
return fmt.Errorf("Cannot delete root entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPath, n := splitPath(name)
|
||||||
|
parent, err := db.get(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM edge WHERE parent_id = ? AND name = ?;", parent.id, n); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the entity with the specified id
|
||||||
|
// Walk the graph to make sure all references to the entity
|
||||||
|
// are removed and return the number of references removed
|
||||||
|
func (db *Database) Purge(id string) (int, error) {
|
||||||
|
db.mux.Lock()
|
||||||
|
defer db.mux.Unlock()
|
||||||
|
|
||||||
|
rollback := func() {
|
||||||
|
db.conn.Exec("ROLLBACK")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.conn.Exec("BEGIN"); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all edges
|
||||||
|
rows, err := db.conn.Exec("DELETE FROM edge WHERE entity_id = ?;", id)
|
||||||
|
if err != nil {
|
||||||
|
rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, err := rows.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete entity
|
||||||
|
if _, err := db.conn.Exec("DELETE FROM entity where id = ?;", id); err != nil {
|
||||||
|
rollback()
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.conn.Exec("COMMIT"); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return int(changes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename an edge for a given path
|
||||||
|
func (db *Database) Rename(currentName, newName string) error {
|
||||||
|
db.mux.Lock()
|
||||||
|
defer db.mux.Unlock()
|
||||||
|
|
||||||
|
parentPath, name := splitPath(currentName)
|
||||||
|
newParentPath, newEdgeName := splitPath(newName)
|
||||||
|
|
||||||
|
if parentPath != newParentPath {
|
||||||
|
return fmt.Errorf("Cannot rename when root paths do not match %s != %s", parentPath, newParentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := db.get(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.conn.Exec("UPDATE edge SET name = ? WHERE parent_id = ? AND name = ?;", newEdgeName, parent.id, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i, err := rows.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
return fmt.Errorf("Cannot locate edge for %s %s", parent.id, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalkMeta struct {
|
||||||
|
Parent *Entity
|
||||||
|
Entity *Entity
|
||||||
|
FullPath string
|
||||||
|
Edge *Edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) children(e *Entity, name string, depth int, entities []WalkMeta) ([]WalkMeta, error) {
|
||||||
|
if e == nil {
|
||||||
|
return entities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.conn.Query("SELECT entity_id, name FROM edge where parent_id = ?;", e.id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var entityId, entityName string
|
||||||
|
if err := rows.Scan(&entityId, &entityName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
child := &Entity{entityId}
|
||||||
|
edge := &Edge{
|
||||||
|
ParentID: e.id,
|
||||||
|
Name: entityName,
|
||||||
|
EntityID: child.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := WalkMeta{
|
||||||
|
Parent: e,
|
||||||
|
Entity: child,
|
||||||
|
FullPath: path.Join(name, edge.Name),
|
||||||
|
Edge: edge,
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = append(entities, meta)
|
||||||
|
|
||||||
|
if depth != 0 {
|
||||||
|
nDepth := depth
|
||||||
|
if depth != -1 {
|
||||||
|
nDepth -= 1
|
||||||
|
}
|
||||||
|
entities, err = db.children(child, meta.FullPath, nDepth, entities)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the entity based on the parent path and name
|
||||||
|
func (db *Database) child(parent *Entity, name string) *Entity {
|
||||||
|
var id string
|
||||||
|
if err := db.conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Entity{id}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the id used to reference this entity
|
||||||
|
func (e *Entity) ID() string {
|
||||||
|
return e.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the paths sorted by depth
|
||||||
|
func (e Entities) Paths() []string {
|
||||||
|
out := make([]string, len(e))
|
||||||
|
var i int
|
||||||
|
for k := range e {
|
||||||
|
out[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sortByDepth(out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
540
graphdb/graphdb_test.go
Normal file
540
graphdb/graphdb_test.go
Normal file
|
@ -0,0 +1,540 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "code.google.com/p/gosqlite/sqlite3"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestDb(t *testing.T) (*Database, string) {
|
||||||
|
p := path.Join(os.TempDir(), "sqlite.db")
|
||||||
|
conn, err := sql.Open("sqlite3", p)
|
||||||
|
db, err := NewDatabase(conn, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return db, p
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroyTestDb(dbPath string) {
|
||||||
|
os.Remove(dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDatabase(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
if db == nil {
|
||||||
|
t.Fatal("Database should not be nil")
|
||||||
|
}
|
||||||
|
db.Close()
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateRootEnity(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
root := db.RootEntity()
|
||||||
|
if root == nil {
|
||||||
|
t.Fatal("Root entity should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRootEntity(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
e := db.Get("/")
|
||||||
|
if e == nil {
|
||||||
|
t.Fatal("Entity should not be nil")
|
||||||
|
}
|
||||||
|
if e.ID() != "0" {
|
||||||
|
t.Fatalf("Enity id should be 0, got %s", e.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetEntityWithDifferentName(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/test", "1")
|
||||||
|
if _, err := db.Set("/other", "1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDuplicateEntity(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
if _, err := db.Set("/foo", "42"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/foo", "43"); err == nil {
|
||||||
|
t.Fatalf("Creating an entry with a duplciate path did not cause an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateChild(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
child, err := db.Set("/db", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if child == nil {
|
||||||
|
t.Fatal("Child should not be nil")
|
||||||
|
}
|
||||||
|
if child.ID() != "1" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllRootChildren(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
for i := 1; i < 6; i++ {
|
||||||
|
a := strconv.Itoa(i)
|
||||||
|
if _, err := db.Set("/"+a, a); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries := db.List("/", -1)
|
||||||
|
if len(entries) != 5 {
|
||||||
|
t.Fatalf("Expect 5 entries for / got %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListAllSubChildren(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
_, err := db.Set("/webapp", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child2, err := db.Set("/db", "2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child4, err := db.Set("/logs", "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/db/logs", child4.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child3, err := db.Set("/sentry", "3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := db.List("/webapp", 1)
|
||||||
|
if len(entries) != 3 {
|
||||||
|
t.Fatalf("Expect 3 entries for / got %d", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = db.List("/webapp", 0)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("Expect 2 entries for / got %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddSelfAsChild(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
child, err := db.Set("/test", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/test/other", child.ID()); err == nil {
|
||||||
|
t.Fatal("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddChildToNonExistantRoot(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
if _, err := db.Set("/myapp", "1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Set("/myapp/proxy/db", "2"); err == nil {
|
||||||
|
t.Fatal("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWalkAll(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
_, err := db.Set("/webapp", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child2, err := db.Set("/db", "2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child4, err := db.Set("/db/logs", "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/logs", child4.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child3, err := db.Set("/sentry", "3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child5, err := db.Set("/gograph", "5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Walk("/", func(p string, e *Entity) error {
|
||||||
|
t.Logf("Path: %s Entity: %s", p, e.ID())
|
||||||
|
return nil
|
||||||
|
}, -1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEntityByPath(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
_, err := db.Set("/webapp", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child2, err := db.Set("/db", "2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child4, err := db.Set("/logs", "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/db/logs", child4.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child3, err := db.Set("/sentry", "3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child5, err := db.Set("/gograph", "5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := db.Get("/webapp/db/logs")
|
||||||
|
if entity == nil {
|
||||||
|
t.Fatal("Entity should not be nil")
|
||||||
|
}
|
||||||
|
if entity.ID() != "4" {
|
||||||
|
t.Fatalf("Expected to get entity with id 4, got %s", entity.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnitiesPaths(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
_, err := db.Set("/webapp", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child2, err := db.Set("/db", "2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child4, err := db.Set("/logs", "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/db/logs", child4.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child3, err := db.Set("/sentry", "3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child5, err := db.Set("/gograph", "5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := db.List("/", -1)
|
||||||
|
for _, p := range out.Paths() {
|
||||||
|
t.Log(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteRootEntity(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
if err := db.Delete("/"); err == nil {
|
||||||
|
t.Fatal("Error should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteEntity(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
_, err := db.Set("/webapp", "1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child2, err := db.Set("/db", "2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
child4, err := db.Set("/logs", "4")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/db/logs", child4.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child3, err := db.Set("/sentry", "3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/db", child2.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
child5, err := db.Set("/gograph", "5")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Delete("/webapp/sentry"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entity := db.Get("/webapp/sentry")
|
||||||
|
if entity != nil {
|
||||||
|
t.Fatal("Entity /webapp/sentry should be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountRefs(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/webapp", "1")
|
||||||
|
|
||||||
|
if db.Refs("1") != 1 {
|
||||||
|
t.Fatal("Expect reference count to be 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Set("/db", "2")
|
||||||
|
db.Set("/webapp/db", "2")
|
||||||
|
if db.Refs("2") != 2 {
|
||||||
|
t.Fatal("Expect reference count to be 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPurgeId(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/webapp", "1")
|
||||||
|
|
||||||
|
if db.Refs("1") != 1 {
|
||||||
|
t.Fatal("Expect reference count to be 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Set("/db", "2")
|
||||||
|
db.Set("/webapp/db", "2")
|
||||||
|
|
||||||
|
count, err := db.Purge("2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 2 {
|
||||||
|
t.Fatal("Expected 2 references to be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRename(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/webapp", "1")
|
||||||
|
|
||||||
|
if db.Refs("1") != 1 {
|
||||||
|
t.Fatal("Expect reference count to be 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Set("/db", "2")
|
||||||
|
db.Set("/webapp/db", "2")
|
||||||
|
|
||||||
|
if db.Get("/webapp/db") == nil {
|
||||||
|
t.Fatal("Cannot find entity at path /webapp/db")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Rename("/webapp/db", "/webapp/newdb"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if db.Get("/webapp/db") != nil {
|
||||||
|
t.Fatal("Entity should not exist at /webapp/db")
|
||||||
|
}
|
||||||
|
if db.Get("/webapp/newdb") == nil {
|
||||||
|
t.Fatal("Cannot find entity at path /webapp/newdb")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateMultipleNames(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/db", "1")
|
||||||
|
if _, err := db.Set("/myapp", "1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Walk("/", func(p string, e *Entity) error {
|
||||||
|
t.Logf("%s\n", p)
|
||||||
|
return nil
|
||||||
|
}, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefPaths(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/webapp", "1")
|
||||||
|
|
||||||
|
db.Set("/db", "2")
|
||||||
|
db.Set("/webapp/db", "2")
|
||||||
|
|
||||||
|
refs := db.RefPaths("2")
|
||||||
|
if len(refs) != 2 {
|
||||||
|
t.Fatalf("Expected reference count to be 2, got %d", len(refs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExistsTrue(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/testing", "1")
|
||||||
|
|
||||||
|
if !db.Exists("/testing") {
|
||||||
|
t.Fatalf("/tesing should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExistsFalse(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/toerhe", "1")
|
||||||
|
|
||||||
|
if db.Exists("/testing") {
|
||||||
|
t.Fatalf("/tesing should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNameWithTrailingSlash(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
db.Set("/todo", "1")
|
||||||
|
|
||||||
|
e := db.Get("/todo/")
|
||||||
|
if e == nil {
|
||||||
|
t.Fatalf("Entity should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrentWrites(t *testing.T) {
|
||||||
|
db, dbpath := newTestDb(t)
|
||||||
|
defer destroyTestDb(dbpath)
|
||||||
|
|
||||||
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
|
save := func(name string, id string) {
|
||||||
|
if _, err := db.Set(fmt.Sprintf("/%s", name), id); err != nil {
|
||||||
|
errs <- err
|
||||||
|
}
|
||||||
|
errs <- nil
|
||||||
|
}
|
||||||
|
purge := func(id string) {
|
||||||
|
if _, err := db.Purge(id); err != nil {
|
||||||
|
errs <- err
|
||||||
|
}
|
||||||
|
errs <- nil
|
||||||
|
}
|
||||||
|
|
||||||
|
save("/1", "1")
|
||||||
|
|
||||||
|
go purge("1")
|
||||||
|
go save("/2", "2")
|
||||||
|
|
||||||
|
any := false
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := <-errs; err != nil {
|
||||||
|
any = true
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
}
|
27
graphdb/sort.go
Normal file
27
graphdb/sort.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type pathSorter struct {
|
||||||
|
paths []string
|
||||||
|
by func(i, j string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortByDepth(paths []string) {
|
||||||
|
s := &pathSorter{paths, func(i, j string) bool {
|
||||||
|
return PathDepth(i) > PathDepth(j)
|
||||||
|
}}
|
||||||
|
sort.Sort(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *pathSorter) Len() int {
|
||||||
|
return len(s.paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *pathSorter) Swap(i, j int) {
|
||||||
|
s.paths[i], s.paths[j] = s.paths[j], s.paths[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *pathSorter) Less(i, j int) bool {
|
||||||
|
return s.by(s.paths[i], s.paths[j])
|
||||||
|
}
|
29
graphdb/sort_test.go
Normal file
29
graphdb/sort_test.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSort(t *testing.T) {
|
||||||
|
paths := []string{
|
||||||
|
"/",
|
||||||
|
"/myreallylongname",
|
||||||
|
"/app/db",
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByDepth(paths)
|
||||||
|
|
||||||
|
if len(paths) != 3 {
|
||||||
|
t.Fatalf("Expected 3 parts got %d", len(paths))
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths[0] != "/app/db" {
|
||||||
|
t.Fatalf("Expected /app/db got %s", paths[0])
|
||||||
|
}
|
||||||
|
if paths[1] != "/myreallylongname" {
|
||||||
|
t.Fatalf("Expected /myreallylongname got %s", paths[1])
|
||||||
|
}
|
||||||
|
if paths[2] != "/" {
|
||||||
|
t.Fatalf("Expected / got %s", paths[2])
|
||||||
|
}
|
||||||
|
}
|
32
graphdb/utils.go
Normal file
32
graphdb/utils.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package graphdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Split p on /
|
||||||
|
func split(p string) []string {
|
||||||
|
return strings.Split(p, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the depth or number of / in a given path
|
||||||
|
func PathDepth(p string) int {
|
||||||
|
parts := split(p)
|
||||||
|
if len(parts) == 2 && parts[1] == "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return len(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPath(p string) (parent, name string) {
|
||||||
|
if p[0] != '/' {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
parent, name = path.Split(p)
|
||||||
|
l := len(parent)
|
||||||
|
if parent[l-1] == '/' {
|
||||||
|
parent = parent[:l-1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in a new issue