package db import ( "database/sql" "errors" "fmt" "os" "os/user" "path/filepath" "axolotl/models" _ "modernc.org/sqlite" ) type DB struct { *sql.DB path string } var database *DB var migrations = []string{ `CREATE TABLE IF NOT EXISTS nodes ( id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT, due_date TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS tags ( node_id TEXT NOT NULL, tag TEXT NOT NULL, PRIMARY KEY (node_id, tag), FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS rels ( from_id TEXT NOT NULL, to_id TEXT NOT NULL, rel_type TEXT NOT NULL, PRIMARY KEY (from_id, to_id, rel_type), FOREIGN KEY (from_id) REFERENCES nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS aliases ( name TEXT PRIMARY KEY, command TEXT NOT NULL )`, `CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag)`, `CREATE INDEX IF NOT EXISTS idx_rels_from ON rels(from_id)`, `CREATE INDEX IF NOT EXISTS idx_rels_to ON rels(to_id)`, } func GetDB() (*DB, error) { if database != nil { return database, nil } dir, err := filepath.Abs(".") if err != nil { return nil, err } for { path := filepath.Join(dir, ".ax.db") if _, err := os.Stat(path); err == nil { database, err = Open(path) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } return database, nil } parent := filepath.Dir(dir) if parent == dir { return nil, errors.New("no .ax.db found (run 'ax init' first)") } path = parent } } func Init(path string) (error) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return err } var err error database, err = Open(path) return err } func Open(path string) (*DB, error) { database, err := sql.Open("sqlite", path) if err != nil { return nil, err } database.Exec("PRAGMA journal_mode=WAL") database.Exec("PRAGMA busy_timeout=5000") database.Exec("PRAGMA foreign_keys=ON") for _, m := range migrations { if _, err := database.Exec(m); err != nil { return nil, err } } return &DB{DB: database, path: path}, nil } func GetCurrentUser() string { if u := os.Getenv("AX_USER"); u != "" { return u } if u, err := user.Current(); err == nil { return u.Username } return "unknown" } func (db *DB) NodeExists(id string) (bool, error) { var exists bool err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM nodes WHERE id = ?)", id).Scan(&exists) return exists, err } func (db *DB) NodeByID(id string) (*models.Node, error) { n := &models.Node{Relations: make(map[string][]string)} err := db.QueryRow( "SELECT id, title, COALESCE(content, ''), COALESCE(due_date, ''), created_at, updated_at FROM nodes WHERE id = ?", id, ).Scan(&n.ID, &n.Title, &n.Content, &n.DueDate, &n.CreatedAt, &n.UpdatedAt) if err != nil { return nil, err } rows, err := db.Query("SELECT tag FROM tags WHERE node_id = ?", id) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var tag string rows.Scan(&tag) n.Tags = append(n.Tags, tag) } rows, err = db.Query("SELECT to_id, rel_type FROM rels WHERE from_id = ?", id) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var toID, relType string rows.Scan(&toID, &relType) n.Relations[relType] = append(n.Relations[relType], toID) } return n, nil }