9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
Post-repair hardware validation pipeline for Proxmox cluster hosts. Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
84 lines
2.2 KiB
Go
84 lines
2.2 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var migrationsFS embed.FS
|
|
|
|
// Open opens the SQLite DB at path, enabling foreign keys and WAL,
|
|
// and applies every embedded migration in filename order.
|
|
func Open(path string) (*sql.DB, error) {
|
|
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)", filepath.ToSlash(path))
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
|
}
|
|
if err := db.Ping(); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("ping sqlite: %w", err)
|
|
}
|
|
if err := migrate(db); err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
return db, nil
|
|
}
|
|
|
|
func migrate(db *sql.DB) error {
|
|
entries, err := fs.ReadDir(migrationsFS, "migrations")
|
|
if err != nil {
|
|
return fmt.Errorf("read migrations: %w", err)
|
|
}
|
|
names := make([]string, 0, len(entries))
|
|
for _, e := range entries {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") {
|
|
names = append(names, e.Name())
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
|
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)`); err != nil {
|
|
return fmt.Errorf("ensure schema_migrations: %w", err)
|
|
}
|
|
|
|
for _, name := range names {
|
|
var applied int
|
|
if err := db.QueryRow(`SELECT COUNT(1) FROM schema_migrations WHERE name = ?`, name).Scan(&applied); err != nil {
|
|
return fmt.Errorf("check migration %s: %w", name, err)
|
|
}
|
|
if applied > 0 {
|
|
continue
|
|
}
|
|
content, err := migrationsFS.ReadFile("migrations/" + name)
|
|
if err != nil {
|
|
return fmt.Errorf("read migration %s: %w", name, err)
|
|
}
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("begin migration %s: %w", name, err)
|
|
}
|
|
if _, err := tx.Exec(string(content)); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("apply migration %s: %w", name, err)
|
|
}
|
|
if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, name); err != nil {
|
|
_ = tx.Rollback()
|
|
return fmt.Errorf("record migration %s: %w", name, err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit migration %s: %w", name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|