diff --git a/cmd/add.go b/cmd/add.go
index 29df469..b0c009f 100644
--- a/cmd/add.go
+++ b/cmd/add.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/models"
"axolotl/output"
"axolotl/service"
@@ -19,12 +18,6 @@ var cTags, cRels []string
var addCmd = &cobra.Command{
Use: "add
", Short: "Create a new node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
- if err != nil {
- fmt.Fprintln(os.Stderr, err)
- return
- }
-
if !slices.ContainsFunc(cTags, func(e string) bool { return strings.HasPrefix(e, "_type::") }) {
cTags = append(cTags, "_type::issue")
}
@@ -49,11 +42,13 @@ var addCmd = &cobra.Command{
rels[models.RelInNamespace] = append(rels[models.RelInNamespace], cfg.GetUser())
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
- if n, err := svc.Create(args[0], cContent, cDue, cTags, rels); err != nil {
+ svc, err := service.GetNodeService(cfg)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "failed to create:", err)
+ } else if n, err := svc.Create(args[0], cContent, cDue, cTags, rels); err != nil {
fmt.Fprintln(os.Stderr, "failed to create:", err)
} else {
- output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
+ output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
}
},
}
diff --git a/cmd/del.go b/cmd/del.go
index f1f16e3..40a4e26 100644
--- a/cmd/del.go
+++ b/cmd/del.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/output"
"axolotl/service"
"bufio"
@@ -16,12 +15,11 @@ var dForce bool
var delCmd = &cobra.Command{
Use: "del ", Short: "Delete a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
+ svc, err := service.GetNodeService(cfg)
if err != nil {
- fmt.Fprintln(os.Stderr, err)
+ fmt.Fprintln(os.Stderr, "failed to create service: %v", err)
return
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
n, err := svc.GetByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, " node not found:", args[0])
diff --git a/cmd/edit.go b/cmd/edit.go
index 3945c66..dbe9bad 100644
--- a/cmd/edit.go
+++ b/cmd/edit.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/output"
"axolotl/service"
"fmt"
@@ -14,12 +13,11 @@ import (
var editCmd = &cobra.Command{
Use: "edit ", Short: "Edit node content in $EDITOR", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
+ svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
n, err := svc.GetByID(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "node not found:", args[0])
@@ -53,7 +51,7 @@ var editCmd = &cobra.Command{
return
}
n, _ = svc.GetByID(args[0])
- output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
+ output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "failed to read temp file:", err)
}
diff --git a/cmd/init.go b/cmd/init.go
index 1e2657f..0bb7e92 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -1,8 +1,8 @@
package cmd
import (
- "axolotl/db"
"axolotl/output"
+ "axolotl/service"
"fmt"
"os"
"path/filepath"
@@ -22,7 +22,7 @@ var initCmd = &cobra.Command{
fmt.Fprintln(os.Stderr, "database already exists:", dbPath)
os.Exit(1)
}
- if err := db.Init(dbPath); err != nil {
+ if err := service.InitNodeService(dbPath); err != nil {
fmt.Fprintln(os.Stderr, "failed to initialize:", err)
os.Exit(1)
}
diff --git a/cmd/list.go b/cmd/list.go
index e313bed..d364bc5 100644
--- a/cmd/list.go
+++ b/cmd/list.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/output"
"axolotl/service"
"fmt"
@@ -17,12 +16,11 @@ var lMention string
var listCmd = &cobra.Command{
Use: "list", Short: "List nodes",
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
+ svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
opts := []service.ListOption{}
if len(lTags) > 0 {
opts = append(opts, service.WithTags(lTags...))
@@ -35,7 +33,7 @@ var listCmd = &cobra.Command{
}
if nodes, err := svc.List(opts...); err == nil {
- output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag)
+ output.PrintNodes(cmd.OutOrStdout(), svc, nodes, jsonFlag)
} else {
fmt.Fprintf(os.Stderr, "err: %v\n", err)
}
diff --git a/cmd/root.go b/cmd/root.go
index 7cac711..786c5fe 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{Use: "ax", Short: "The axolotl issue tracker"}
func Execute() {
var err error
- cfg, err = service.LoadConfig()
+ cfg, err = service.LoadConfigFile()
if err != nil {
fmt.Fprintln(os.Stderr, "failed to load config:", err)
os.Exit(1)
@@ -27,18 +27,48 @@ func Execute() {
}
}
+func expandAlias(alias *service.Alias, args []string, currentUser string) []string {
+ cmd := alias.Command
+ cmd = strings.ReplaceAll(cmd, "$me", currentUser)
+
+ parts := strings.Fields(cmd)
+ var result []string
+
+ for _, part := range parts {
+ if part == "$@" {
+ result = append(result, args...)
+ continue
+ }
+
+ hasCatchAll := strings.Contains(part, "$@")
+ replaced := part
+
+ if hasCatchAll {
+ replaced = strings.ReplaceAll(replaced, "$@", strings.Join(args, " "))
+ }
+
+ for i := len(args) - 1; i >= 0; i-- {
+ placeholder := fmt.Sprintf("$%d", i+1)
+ replaced = strings.ReplaceAll(replaced, placeholder, args[i])
+ }
+
+ result = append(result, replaced)
+ }
+
+ return result
+}
+
func registerAliasCommands() {
rootCmd.AddGroup(&cobra.Group{ID: "aliases", Title: "Aliases:"})
aliases, _ := cfg.ListAliases()
for _, a := range aliases {
- a := a
rootCmd.AddCommand(&cobra.Command{
Use: a.Name,
Short: a.Description,
GroupID: "aliases",
DisableFlagParsing: true,
Run: func(cmd *cobra.Command, args []string) {
- expanded := service.ExpandAlias(a, args, cfg.GetUser())
+ expanded := expandAlias(a, args, cfg.GetUser())
rootCmd.SetArgs(transformArgs(expanded))
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
diff --git a/cmd/show.go b/cmd/show.go
index 8390fb8..fbe619b 100644
--- a/cmd/show.go
+++ b/cmd/show.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/output"
"axolotl/service"
"fmt"
@@ -13,14 +12,13 @@ import (
var showCmd = &cobra.Command{
Use: "show ", Short: "Show node details", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
+ svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
if n, err := svc.GetByID(args[0]); err == nil {
- output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
+ output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "node not found:", args[0])
}
diff --git a/cmd/update.go b/cmd/update.go
index d24d279..3b3b4d8 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -1,7 +1,6 @@
package cmd
import (
- "axolotl/db"
"axolotl/models"
"axolotl/output"
"axolotl/service"
@@ -14,7 +13,7 @@ import (
)
var (
- uTitle, uContent, uDue string
+ uTitle, uContent, uDue string
uClearDue bool
uAddTags, uRmTags, uAddRels, uRmRels []string
)
@@ -22,12 +21,11 @@ var (
var updateCmd = &cobra.Command{
Use: "update ", Short: "Update a node", Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- d, err := db.GetDB()
+ svc, err := service.GetNodeService(cfg)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
- svc := service.NewSQLiteNodeService(d.DB, cfg.GetUser())
node, err := svc.GetByID(args[0])
if err != nil {
@@ -65,7 +63,7 @@ var updateCmd = &cobra.Command{
} else if slices.Contains(uAddTags, "_status::open") {
uRmTags = append(uRmTags, "_status::done")
}
-
+
for _, prefix := range []string{"_type::", "_status::", "_prio::", "_namespace::"} {
if slices.ContainsFunc(uAddTags, func(e string) bool { return strings.HasPrefix(e, prefix) }) {
for _, existing := range node.Tags {
@@ -76,7 +74,6 @@ var updateCmd = &cobra.Command{
}
}
-
if cmd.Flags().Changed("title") {
node.Title = uTitle
}
@@ -117,7 +114,7 @@ var updateCmd = &cobra.Command{
return
}
if n, err := svc.GetByID(args[0]); err == nil {
- output.PrintNode(cmd.OutOrStdout(), n, jsonFlag)
+ output.PrintNode(cmd.OutOrStdout(), svc, n, jsonFlag)
} else {
fmt.Fprintln(os.Stderr, "failed to fetch node:", err)
}
diff --git a/db/db.go b/db/db.go
deleted file mode 100644
index 0dedbd9..0000000
--- a/db/db.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package db
-
-import (
- "database/sql"
- "errors"
- "fmt"
- "os"
- "path/filepath"
-
- _ "modernc.org/sqlite"
-)
-
-type DB struct {
- *sql.DB
- path string
-}
-
-var (
- database *DB
- 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 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 {
- if _, err := os.Stat(filepath.Join(dir, ".ax.db")); err == nil {
- if database, err = Open(filepath.Join(dir, ".ax.db")); err != nil {
- return nil, fmt.Errorf("failed to open database: %w", err)
- }
- return database, nil
- }
- if parent := filepath.Dir(dir); parent == dir {
- return nil, errors.New("no .ax.db found (run 'ax init' first)")
- } else {
- dir = parent
- }
- }
-}
-
-func Init(path string) error {
- if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
- return err
- }
- var err error
- database, err = Open(path)
- return err
-}
-
-func Open(path string) (*DB, error) {
- db, err := sql.Open("sqlite", path)
- if err != nil {
- return nil, err
- }
- for _, q := range append([]string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000", "PRAGMA foreign_keys=ON"}, migrations...) {
- if _, err := db.Exec(q); err != nil {
- return nil, err
- }
- }
- return &DB{DB: db, path: path}, nil
-}
-
diff --git a/db/rel.go b/db/rel.go
deleted file mode 100644
index e6fe95b..0000000
--- a/db/rel.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package db
-
-import "axolotl/models"
-
-func (db *DB) GetRelNames(n *models.Node, r models.RelType) ([]string, error) {
- ids := n.Relations[string(r)]
- if len(ids) == 0 {
- return nil, nil
- }
- result := make([]string, 0, len(ids))
- for _, id := range ids {
- var title string
- if err := db.QueryRow("SELECT title FROM nodes WHERE id = ?", id).Scan(&title); err != nil {
- return nil, err
- }
- result = append(result, title)
- }
- return result, nil
-}
diff --git a/models/node.go b/models/node.go
index 0c17883..5b8ee8c 100644
--- a/models/node.go
+++ b/models/node.go
@@ -13,18 +13,6 @@ type Node struct {
Relations map[string][]string `json:"relations,omitempty"`
}
-type RelType string
-
-const (
- RelBlocks RelType = "blocks"
- RelSubtask RelType = "subtask"
- RelRelated RelType = "related"
- RelCreated RelType = "created"
- RelAssignee RelType = "assignee"
- RelInNamespace RelType = "in_namespace"
- RelMentions RelType = "mentions"
-)
-
func (n *Node) GetProperty(k string) string {
for _, t := range n.Tags {
if strings.HasPrefix(t, "_") {
diff --git a/models/rel_type.go b/models/rel_type.go
new file mode 100644
index 0000000..bd59e8d
--- /dev/null
+++ b/models/rel_type.go
@@ -0,0 +1,13 @@
+package models
+
+type RelType string
+
+const (
+ RelBlocks RelType = "blocks"
+ RelSubtask RelType = "subtask"
+ RelRelated RelType = "related"
+ RelCreated RelType = "created"
+ RelAssignee RelType = "assignee"
+ RelInNamespace RelType = "in_namespace"
+ RelMentions RelType = "mentions"
+)
diff --git a/output/output.go b/output/output.go
index 1f28a2a..eb7b2f3 100644
--- a/output/output.go
+++ b/output/output.go
@@ -1,7 +1,6 @@
package output
import (
- "axolotl/db"
"axolotl/models"
"axolotl/service"
"encoding/json"
@@ -59,21 +58,7 @@ const (
iconNamespace = "\uf07b"
)
-func render(rm RenderMap, key string, short bool) string {
- v, ok := rm[key]
- if !ok {
- v, ok = rm[""]
- if !ok {
- return ""
- }
- }
- if short {
- return v.c.Sprint(v.s)
- }
- return v.c.Sprint(v.l)
-}
-
-func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
+func PrintNodes(w io.Writer, svc service.NodeService, nodes []*models.Node, jsonOut bool) error {
if jsonOut {
return json.NewEncoder(w).Encode(nodes)
}
@@ -82,11 +67,6 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
return nil
}
- d, err := db.GetDB()
- if err != nil {
- return err
- }
-
fmt.Fprintln(w)
sort.Slice(nodes, func(i, j int) bool {
si, sj := nodes[i].GetProperty("status"), nodes[j].GetProperty("status")
@@ -97,9 +77,14 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
})
for _, n := range nodes {
- ns_rels, err := d.GetRelNames(n, models.RelInNamespace)
- if err != nil {
- return err
+ ns_rel_node_ids := n.Relations[string(models.RelInNamespace)]
+ ns_rel_node_titles := make([]string, 0, len(ns_rel_node_ids))
+ for _, id := range ns_rel_node_ids {
+ ns_rel_node, err := svc.GetByID(id)
+ if err != nil {
+ fmt.Fprintf(w, "err: %v", err)
+ }
+ ns_rel_node_titles = append(ns_rel_node_titles, ns_rel_node.Title)
}
fmt.Fprintf(w, " %s %s %s %s %s %s",
cDim.Sprint(n.ID),
@@ -107,7 +92,7 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
render(statusRM, n.GetProperty("status"), true),
render(typeRM, n.GetProperty("type"), true),
cTitle.Sprint(truncate(n.Title, 80)),
- cDim.Sprint("["+strings.Join(ns_rels, ",")+"]"),
+ cDim.Sprint("["+strings.Join(ns_rel_node_titles, ",")+"]"),
)
tags := n.GetDisplayTags()
if len(tags) > 0 {
@@ -119,7 +104,7 @@ func PrintNodes(w io.Writer, nodes []*models.Node, jsonOut bool) error {
return nil
}
-func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error {
+func PrintNode(w io.Writer, svc service.NodeService, n *models.Node, jsonOut bool) error {
if jsonOut {
return json.NewEncoder(w).Encode(n)
}
@@ -138,20 +123,17 @@ func PrintNode(w io.Writer, n *models.Node, jsonOut bool) error {
fmt.Fprintf(w, "\n tags: %s\n", cPrimary.Sprint(strings.Join(tags, " • ")))
}
- if db, err := db.GetDB(); err != nil {
- fmt.Fprintf(w, "failed to attach to db: %v", err)
- } else {
- for relType := range n.Relations {
- names, err := db.GetRelNames(n, models.RelType(relType))
+ for relType := range n.Relations {
+ rel_node_ids := n.Relations[string(relType)]
+ if len(rel_node_ids) > 0 {
+ fmt.Fprintf(w, "\n %s\n", string(relType))
+ }
+ for _, id := range rel_node_ids {
+ rel_node, err := svc.GetByID(id)
if err != nil {
fmt.Fprintf(w, "err: %v", err)
}
- if len(names) > 0 {
- fmt.Fprintf(w, "\n %s\n", string(relType))
- }
- for _, name := range names {
- fmt.Fprintf(w, " %s %s\n", relIcons[relType], name)
- }
+ fmt.Fprintf(w, " %s %s\n", relIcons[relType], rel_node.Title)
}
}
@@ -201,6 +183,20 @@ func PrintAction(w io.Writer, action, detail string, isError bool) {
fmt.Fprintln(w, cGood.Sprint(icon+" "+action+" ")+cDim.Sprint(detail))
}
+func render(rm RenderMap, key string, short bool) string {
+ v, ok := rm[key]
+ if !ok {
+ v, ok = rm[""]
+ if !ok {
+ return ""
+ }
+ }
+ if short {
+ return v.c.Sprint(v.s)
+ }
+ return v.c.Sprint(v.l)
+}
+
func truncate(s string, max int) string {
if len(s) <= max {
return s
diff --git a/service/fileconfig.go b/service/config_file.go
similarity index 73%
rename from service/fileconfig.go
rename to service/config_file.go
index ee5edae..ee0cf65 100644
--- a/service/fileconfig.go
+++ b/service/config_file.go
@@ -3,12 +3,10 @@ package service
import (
"encoding/json"
"errors"
- "fmt"
"os"
"os/user"
"path/filepath"
"slices"
- "strings"
)
type fileConfig struct {
@@ -18,13 +16,12 @@ type fileConfig struct {
}
var defaultAliases = []*Alias{
- {Name: "mine", Command: "list --assignee $me --tag _status::open", Description: "Show open tasks assigned to you"},
- {Name: "due", Command: "list --tag _status::open --tag _due", Description: "Show open tasks with due dates"},
- {Name: "new", Command: "add $@", Description: "Create a new task"},
+ {Name: "mine", Command: "list --assignee $me --tag _type::issue --tag _status::open", Description: "Show open issues assigned to you"},
+ {Name: "due", Command: "list --tag _type::issue --tag _status::open", Description: "Show open issues"},
{Name: "inbox", Command: "list --mention $me", Description: "Show your inbox"},
}
-func LoadConfig() (Config, error) {
+func LoadConfigFile() (Config, error) {
path, err := findConfigPath()
if err != nil {
return nil, err
@@ -153,34 +150,3 @@ func (c *fileConfig) Save() error {
}
return os.WriteFile(c.path, data, 0644)
}
-
-func ExpandAlias(alias *Alias, args []string, currentUser string) []string {
- cmd := alias.Command
- cmd = strings.ReplaceAll(cmd, "$me", currentUser)
-
- parts := strings.Fields(cmd)
- var result []string
-
- for _, part := range parts {
- if part == "$@" {
- result = append(result, args...)
- continue
- }
-
- hasCatchAll := strings.Contains(part, "$@")
- replaced := part
-
- if hasCatchAll {
- replaced = strings.ReplaceAll(replaced, "$@", strings.Join(args, " "))
- }
-
- for i := len(args) - 1; i >= 0; i-- {
- placeholder := fmt.Sprintf("$%d", i+1)
- replaced = strings.ReplaceAll(replaced, placeholder, args[i])
- }
-
- result = append(result, replaced)
- }
-
- return result
-}
diff --git a/service/node_service.go b/service/node_service.go
index d5d1588..e68c56f 100644
--- a/service/node_service.go
+++ b/service/node_service.go
@@ -12,6 +12,18 @@ type NodeService interface {
CanClose(id string) (bool, []string, error)
}
+func InitNodeService(path string) error {
+ return InitSqliteDB(path)
+}
+
+func GetNodeService(cfg Config) (NodeService, error) {
+ db, err := GetSqliteDB(cfg)
+ if err != nil {
+ return nil, err
+ }
+ return &sqliteNodeService{db: db, userID: cfg.GetUser()}, nil
+}
+
type listFilter struct {
tagPrefixes []string
assignee string
diff --git a/service/node_service_sqlite.go b/service/node_service_sqlite.go
index 7cca4eb..338bfd3 100644
--- a/service/node_service_sqlite.go
+++ b/service/node_service_sqlite.go
@@ -3,10 +3,16 @@ package service
import (
"axolotl/models"
"database/sql"
+ "errors"
+ "fmt"
"math/rand"
+ "os"
+ "path/filepath"
"slices"
"strings"
"time"
+
+ _ "modernc.org/sqlite"
)
type sqliteNodeService struct {
@@ -14,8 +20,50 @@ type sqliteNodeService struct {
userID string
}
-func NewSQLiteNodeService(db *sql.DB, userID string) NodeService {
- return &sqliteNodeService{db: db, userID: userID}
+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 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 InitSqliteDB(path string) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ var err error
+ db, err := sql.Open("sqlite", path)
+ if err != nil {
+ return err
+ }
+ for _, q := range append([]string{"PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=5000", "PRAGMA foreign_keys=ON"}, migrations...) {
+ if _, err := db.Exec(q); err != nil {
+ return err
+ }
+ }
+ return err
+}
+
+func GetSqliteDB(cfg Config) (*sql.DB, error) {
+ dir, err := filepath.Abs(".")
+ if err != nil {
+ return nil, err
+ }
+ for {
+ dbpath := filepath.Join(dir, ".ax.db")
+ if _, err := os.Stat(dbpath); err == nil {
+ db, err := sql.Open("sqlite", dbpath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open database: %w", err)
+ }
+ return db, nil
+ }
+ if parent := filepath.Dir(dir); parent == dir {
+ return nil, errors.New("no .ax.db found (run 'ax init' first)")
+ } else {
+ dir = parent
+ }
+ }
}
func (s *sqliteNodeService) GetByID(id string) (*models.Node, error) {