Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
// +build cgo
package graphdb
import "database/sql"
// NewSqliteConn opens a connection to a sqlite
// database.
func NewSqliteConn(root string) (*Database, error) {
conn, err := sql.Open("sqlite3", root)
if err != nil {
return nil, err
}
return NewDatabase(conn)
}

View File

@@ -0,0 +1,7 @@
// +build cgo,!windows
package graphdb
import (
_ "github.com/mattn/go-sqlite3" // registers sqlite
)

View File

@@ -0,0 +1,7 @@
// +build cgo,windows
package graphdb
import (
_ "github.com/mattn/go-sqlite3" // registers sqlite
)

View File

@@ -0,0 +1,8 @@
// +build !cgo
package graphdb
// NewSqliteConn return a new sqlite connection.
func NewSqliteConn(root string) (*Database, error) {
panic("Not implemented")
}

View File

@@ -0,0 +1,551 @@
package graphdb
import (
"database/sql"
"fmt"
"path"
"strings"
"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
}
// Entities stores the list of entities.
type Entities map[string]*Entity
// Edges stores the relationships between entities.
type Edges []*Edge
// WalkFunc is a function invoked to process an individual entity.
type WalkFunc func(fullPath string, entity *Entity) error
// Database is a graph database for storing entities and their relationships.
type Database struct {
conn *sql.DB
mux sync.RWMutex
}
// IsNonUniqueNameError processes the error to check if it's caused by
// a constraint violation.
// This is necessary because the error isn't the same across various
// sqlite versions.
func IsNonUniqueNameError(err error) bool {
str := err.Error()
// sqlite 3.7.17-1ubuntu1 returns:
// Set failure: Abort due to constraint violation: columns parent_id, name are not unique
if strings.HasSuffix(str, "name are not unique") {
return true
}
// sqlite-3.8.3-1.fc20 returns:
// Set failure: Abort due to constraint violation: UNIQUE constraint failed: edge.parent_id, edge.name
if strings.Contains(str, "UNIQUE constraint failed") && strings.Contains(str, "edge.name") {
return true
}
// sqlite-3.6.20-1.el6 returns:
// Set failure: Abort due to constraint violation: constraint failed
if strings.HasSuffix(str, "constraint failed") {
return true
}
return false
}
// NewDatabase creates a new graph database initialized with a root entity.
func NewDatabase(conn *sql.DB) (*Database, error) {
if conn == nil {
return nil, fmt.Errorf("Database connection cannot be nil")
}
db := &Database{conn: conn}
// Create root entities
tx, err := conn.Begin()
if err != nil {
return nil, err
}
if _, err := tx.Exec(createEntityTable); err != nil {
return nil, err
}
if _, err := tx.Exec(createEdgeTable); err != nil {
return nil, err
}
if _, err := tx.Exec(createEdgeIndices); err != nil {
return nil, err
}
if _, err := tx.Exec("DELETE FROM entity where id = ?", "0"); err != nil {
tx.Rollback()
return nil, err
}
if _, err := tx.Exec("INSERT INTO entity (id) VALUES (?);", "0"); err != nil {
tx.Rollback()
return nil, err
}
if _, err := tx.Exec("DELETE FROM edge where entity_id=? and name=?", "0", "/"); err != nil {
tx.Rollback()
return nil, err
}
if _, err := tx.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", "0", "/"); err != nil {
tx.Rollback()
return nil, err
}
if err := tx.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()
tx, err := db.conn.Begin()
if err != nil {
return nil, err
}
var entityID string
if err := tx.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityID); err != nil {
if err == sql.ErrNoRows {
if _, err := tx.Exec("INSERT INTO entity (id) VALUES(?);", id); err != nil {
tx.Rollback()
return nil, err
}
} else {
tx.Rollback()
return nil, err
}
}
e := &Entity{id}
parentPath, name := splitPath(fullPath)
if err := db.setEdge(parentPath, name, e, tx); err != nil {
tx.Rollback()
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return e, nil
}
// Exists returns 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, tx *sql.Tx) 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 := tx.Exec("INSERT INTO edge (parent_id, name, entity_id) VALUES (?,?,?);", parent.id, name, e.id); err != nil {
return err
}
return nil
}
// RootEntity returns the root "/" entity for the database.
func (db *Database) RootEntity() *Entity {
return &Entity{
id: "0",
}
}
// Get returns 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
}
// Children returns 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)
}
// Parents returns the parents of a specified entity.
func (db *Database) Parents(name string) ([]string, error) {
db.mux.RLock()
defer db.mux.RUnlock()
e, err := db.get(name)
if err != nil {
return nil, err
}
return db.parents(e)
}
// Refs returns the reference 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
}
// RefPaths returns 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
}
// Purge removes 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()
tx, err := db.conn.Begin()
if err != nil {
return -1, err
}
// Delete all edges
rows, err := tx.Exec("DELETE FROM edge WHERE entity_id = ?;", id)
if err != nil {
tx.Rollback()
return -1, err
}
changes, err := rows.RowsAffected()
if err != nil {
return -1, err
}
// Clear who's using this id as parent
refs, err := tx.Exec("DELETE FROM edge WHERE parent_id = ?;", id)
if err != nil {
tx.Rollback()
return -1, err
}
refsCount, err := refs.RowsAffected()
if err != nil {
return -1, err
}
// Delete entity
if _, err := tx.Exec("DELETE FROM entity where id = ?;", id); err != nil {
tx.Rollback()
return -1, err
}
if err := tx.Commit(); err != nil {
return -1, err
}
return int(changes + refsCount), 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
}
// WalkMeta stores the walk metadata.
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--
}
entities, err = db.children(child, meta.FullPath, nDepth, entities)
if err != nil {
return nil, err
}
}
}
return entities, nil
}
func (db *Database) parents(e *Entity) (parents []string, err error) {
if e == nil {
return parents, nil
}
rows, err := db.conn.Query("SELECT parent_id FROM edge where entity_id = ?;", e.id)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var parentID string
if err := rows.Scan(&parentID); err != nil {
return nil, err
}
parents = append(parents, parentID)
}
return parents, 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}
}
// ID returns the id used to reference this entity.
func (e *Entity) ID() string {
return e.id
}
// Paths returns 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
}

View File

@@ -0,0 +1,657 @@
package graphdb
import (
"database/sql"
"fmt"
"os"
"path"
"strconv"
"testing"
_ "github.com/mattn/go-sqlite3"
)
func newTestDb(t *testing.T) (*Database, string) {
p := path.Join(os.TempDir(), "sqlite.db")
conn, err := sql.Open("sqlite3", p)
db, err := NewDatabase(conn)
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 TestCreateRootEntity(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("Entity 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 duplicate 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 TestParents(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)
}
}
for i := 6; i < 11; i++ {
a := strconv.Itoa(i)
p := strconv.Itoa(i - 5)
key := fmt.Sprintf("/%s/%s", p, a)
if _, err := db.Set(key, a); err != nil {
t.Fatal(err)
}
parents, err := db.Parents(key)
if err != nil {
t.Fatal(err)
}
if len(parents) != 1 {
t.Fatalf("Expected 1 entry for %s got %d", key, len(parents))
}
if parents[0] != p {
t.Fatalf("ID %s received, %s expected", parents[0], p)
}
}
}
func TestChildren(t *testing.T) {
db, dbpath := newTestDb(t)
defer destroyTestDb(dbpath)
str := "/"
for i := 1; i < 6; i++ {
a := strconv.Itoa(i)
if _, err := db.Set(str+a, a); err != nil {
t.Fatal(err)
}
str = str + a + "/"
}
str = "/"
for i := 10; i < 30; i++ { // 20 entities
a := strconv.Itoa(i)
if _, err := db.Set(str+a, a); err != nil {
t.Fatal(err)
}
str = str + a + "/"
}
entries, err := db.Children("/", 5)
if err != nil {
t.Fatal(err)
}
if len(entries) != 11 {
t.Fatalf("Expect 11 entries for / got %d", len(entries))
}
entries, err = db.Children("/", 20)
if err != nil {
t.Fatal(err)
}
if len(entries) != 25 {
t.Fatalf("Expect 25 entries for / got %d", len(entries))
}
}
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 TestAddChildToNonExistentRoot(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 c := db.Refs("1"); c != 1 {
t.Fatalf("Expect reference count to be 1, got %d", c)
}
db.Set("/db", "2")
db.Set("/webapp/db", "2")
count, err := db.Purge("2")
if err != nil {
t.Fatal(err)
}
if count != 2 {
t.Fatalf("Expected 2 references to be removed, got %d", count)
}
}
// Regression test https://github.com/hyperhq/hypercli/issues/12334
func TestPurgeIdRefPaths(t *testing.T) {
db, dbpath := newTestDb(t)
defer destroyTestDb(dbpath)
db.Set("/webapp", "1")
db.Set("/db", "2")
db.Set("/db/webapp", "1")
if c := db.Refs("1"); c != 2 {
t.Fatalf("Expected 2 reference for webapp, got %d", c)
}
if c := db.Refs("2"); c != 1 {
t.Fatalf("Expected 1 reference for db, got %d", c)
}
if rp := db.RefPaths("2"); len(rp) != 1 {
t.Fatalf("Expected 1 reference path for db, got %d", len(rp))
}
count, err := db.Purge("2")
if err != nil {
t.Fatal(err)
}
if count != 2 {
t.Fatalf("Expected 2 rows to be removed, got %d", count)
}
if c := db.Refs("2"); c != 0 {
t.Fatalf("Expected 0 reference for db, got %d", c)
}
if c := db.Refs("1"); c != 1 {
t.Fatalf("Expected 1 reference for webapp, got %d", c)
}
}
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.Fail()
}
}

27
vendor/github.com/hyperhq/hypercli/pkg/graphdb/sort.go generated vendored Normal file
View 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])
}

View 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])
}
}

View File

@@ -0,0 +1,32 @@
package graphdb
import (
"path"
"strings"
)
// Split p on /
func split(p string) []string {
return strings.Split(p, "/")
}
// PathDepth 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
}