package gograph import ( _ "code.google.com/p/gosqlite/sqlite3" "database/sql" "fmt" "os" "path" ) 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") ); CREATE UNIQUE INDEX "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 { dbPath string rootID string } // Create a new graph database initialized with a root entity func NewDatabase(dbPath, rootId string) (*Database, error) { db := &Database{dbPath, rootId} if _, err := os.Stat(dbPath); err == nil { return db, nil } conn, err := db.openConn() if err != nil { return nil, err } defer conn.Close() if _, err := conn.Exec(createEntityTable); err != nil { return nil, err } if _, err := conn.Exec(createEdgeTable); 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 (?);", rootId); err != nil { rollback() return nil, err } if _, err := conn.Exec("INSERT INTO edge (entity_id, name) VALUES(?,?);", rootId, "/"); err != nil { rollback() return nil, err } if _, err := conn.Exec("COMMIT"); err != nil { return nil, err } return db, nil } // Set the entity id for a given path func (db *Database) Set(fullPath, id string) (*Entity, error) { conn, err := db.openConn() if err != nil { return nil, err } defer conn.Close() rollback := func() { conn.Exec("ROLLBACK") } if _, err := conn.Exec("BEGIN"); err != nil { return nil, err } var entityId string if err := conn.QueryRow("SELECT id FROM entity WHERE id = ?;", id).Scan(&entityId); err != nil { if err == sql.ErrNoRows { if _, err := 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(conn, parentPath, name, e); err != nil { rollback() return nil, err } if _, err := conn.Exec("COMMIT"); err != nil { return nil, err } return e, nil } func (db *Database) setEdge(conn *sql.DB, parentPath, name string, e *Entity) error { parent, err := db.get(conn, parentPath) if err != nil { return err } if parent.id == e.id { return fmt.Errorf("Cannot set self as child") } if _, err := 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: db.rootID, } } // Return the entity for a given path func (db *Database) Get(name string) *Entity { conn, err := db.openConn() if err != nil { return nil } e, err := db.get(conn, name) if err != nil { return nil } return e } func (db *Database) get(conn *sql.DB, 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] next := db.child(conn, e, p) if next == nil { return nil, fmt.Errorf("Cannot find child") } 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 { out := Entities{} conn, err := db.openConn() if err != nil { return out } defer conn.Close() for c := range db.children(conn, name, depth) { out[c.FullPath] = c.Entity } return out } func (db *Database) Walk(name string, walkFunc WalkFunc, depth int) error { conn, err := db.openConn() if err != nil { return err } defer conn.Close() for c := range db.children(conn, name, depth) { if err := walkFunc(c.FullPath, c.Entity); err != nil { return err } } return nil } // Return the refrence count for a specified id func (db *Database) Refs(id string) int { conn, err := db.openConn() if err != nil { return -1 } defer conn.Close() var count int if err := 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 { refs := Edges{} conn, err := db.openConn() if err != nil { return refs } defer conn.Close() rows, err := 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 { if name == "/" { return fmt.Errorf("Cannot delete root entity") } conn, err := db.openConn() if err != nil { return err } defer conn.Close() parentPath, n := splitPath(name) parent, err := db.get(conn, parentPath) if err != nil { return err } if _, err := 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) { conn, err := db.openConn() if err != nil { return -1, err } defer conn.Close() rollback := func() { conn.Exec("ROLLBACK") } if _, err := conn.Exec("BEGIN"); err != nil { return -1, err } // Delete all edges rows, err := 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 := conn.Exec("DELETE FROM entity where id = ?;", id); err != nil { rollback() return -1, err } if _, err := 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 { 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) } conn, err := db.openConn() if err != nil { return err } defer conn.Close() parent, err := db.get(conn, parentPath) if err != nil { return err } rows, err := 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(conn *sql.DB, name string, depth int) <-chan WalkMeta { out := make(chan WalkMeta) e, err := db.get(conn, name) if err != nil { close(out) return out } go func() { rows, err := conn.Query("SELECT entity_id, name FROM edge where parent_id = ?;", e.id) if err != nil { close(out) } defer rows.Close() for rows.Next() { var entityId, entityName string if err := rows.Scan(&entityId, &entityName); err != nil { // Log error continue } 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, } out <- meta if depth == 0 { continue } nDepth := depth if depth != -1 { nDepth -= 1 } sc := db.children(conn, meta.FullPath, nDepth) for c := range sc { out <- c } } close(out) }() return out } // Return the entity based on the parent path and name func (db *Database) child(conn *sql.DB, parent *Entity, name string) *Entity { var id string if err := conn.QueryRow("SELECT entity_id FROM edge WHERE parent_id = ? AND name = ?;", parent.id, name).Scan(&id); err != nil { return nil } return &Entity{id} } func (db *Database) openConn() (*sql.DB, error) { return sql.Open("sqlite3", db.dbPath) } // 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 }