package tangledalertbot import ( "database/sql" "errors" "fmt" "log/slog" "os" _ "github.com/glebarez/go-sqlite" ) // Database is a sqlite database type Database struct { db *sql.DB } // NewDatabase will open a new database. It will ping the database to ensure it is available and error if not func NewDatabase(dbPath string) (*Database, error) { if dbPath != ":memory:" { err := createDbFile(dbPath) if err != nil { return nil, fmt.Errorf("create db file: %w", err) } } db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("open database: %w", err) } err = db.Ping() if err != nil { return nil, fmt.Errorf("ping db: %w", err) } err = createIssuesTable(db) if err != nil { return nil, fmt.Errorf("creating issues table: %w", err) } err = createCommentsTable(db) if err != nil { return nil, fmt.Errorf("creating comments table: %w", err) } return &Database{db: db}, nil } // Close will cleanly stop the database connection func (d *Database) Close() { err := d.db.Close() if err != nil { slog.Error("failed to close db", "error", err) } } func createDbFile(dbFilename string) error { if _, err := os.Stat(dbFilename); !errors.Is(err, os.ErrNotExist) { return nil } f, err := os.Create(dbFilename) if err != nil { return fmt.Errorf("create db file : %w", err) } err = f.Close() if err != nil { return fmt.Errorf("failed to close DB file: %w", err) } return nil } func createIssuesTable(db *sql.DB) error { createTableSQL := `CREATE TABLE IF NOT EXISTS issues ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "authorDid" TEXT, "rkey" TEXT, "title" TEXT, "body" TEXT, "repo" TEXT, "createdAt" integer NOT NULL, UNIQUE(authorDid,rkey) );` slog.Info("Create issues table...") statement, err := db.Prepare(createTableSQL) if err != nil { return fmt.Errorf("prepare DB statement to create issues table: %w", err) } _, err = statement.Exec() if err != nil { return fmt.Errorf("exec sql statement to create issues table: %w", err) } slog.Info("issues table created") return nil } func createCommentsTable(db *sql.DB) error { createTableSQL := `CREATE TABLE IF NOT EXISTS comments ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "authorDid" TEXT, "rkey" TEXT, "body" TEXT, "issue" TEXT, "replyTo" TEXT, "createdAt" integer NOT NULL, UNIQUE(authorDid,rkey) );` slog.Info("Create comments table...") statement, err := db.Prepare(createTableSQL) if err != nil { return fmt.Errorf("prepare DB statement to create comments table: %w", err) } _, err = statement.Exec() if err != nil { return fmt.Errorf("exec sql statement to create comments table: %w", err) } slog.Info("comments table created") return nil } // CreateIssue will insert a issue into a database func (d *Database) CreateIssue(issue Issue) error { sql := `INSERT INTO issues (authorDid, rkey, title, body, repo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` _, err := d.db.Exec(sql, issue.AuthorDID, issue.RKey, issue.Title, issue.Body, issue.Repo, issue.CreatedAt) if err != nil { return fmt.Errorf("exec insert issue: %w", err) } return nil } // CreateComment will insert a comment into a database func (d *Database) CreateComment(comment Comment) error { sql := `INSERT INTO comments (authorDid, rkey, body, issue, replyTo, createdAt) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(authorDid, rkey) DO NOTHING;` _, err := d.db.Exec(sql, comment.AuthorDID, comment.RKey, comment.Body, comment.Issue, comment.ReplyTo, comment.CreatedAt) if err != nil { return fmt.Errorf("exec insert comment: %w", err) } return nil }