initial public release
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFS embed.FS
|
||||
|
||||
// DB wraps the SQLite connection and migrations.
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// NewDB opens or creates the SQLite database and runs migrations.
|
||||
func NewDB(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// SQLite writer serialization: one open conn avoids BUSY retries under parallel goroutines.
|
||||
conn.SetMaxOpenConns(1)
|
||||
|
||||
// Test connection
|
||||
if err := conn.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
if _, err := conn.Exec("PRAGMA foreign_keys = ON"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
db := &DB{conn: conn}
|
||||
|
||||
// Run migrations
|
||||
if err := db.runMigrations(); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// runMigrations applies all SQL migrations from the migrations/ directory.
|
||||
// Each migration is applied exactly once — tracked via the schema_migrations table.
|
||||
// This is required because ALTER TABLE ADD COLUMN is not idempotent in SQLite.
|
||||
func (db *DB) runMigrations() error {
|
||||
if _, err := db.conn.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("failed to create schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
applied := make(map[string]bool)
|
||||
rows, err := db.conn.Query(`SELECT name FROM schema_migrations`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read schema_migrations: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
rows.Close()
|
||||
return fmt.Errorf("failed to scan migration name: %w", err)
|
||||
}
|
||||
applied[name] = true
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
entries, err := fs.ReadDir(migrationFS, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migrations: %w", err)
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") {
|
||||
files = append(files, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(files)
|
||||
|
||||
for _, file := range files {
|
||||
if applied[file] {
|
||||
continue
|
||||
}
|
||||
content, err := fs.ReadFile(migrationFS, fmt.Sprintf("migrations/%s", file))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migration %s: %w", file, err)
|
||||
}
|
||||
tx, err := db.conn.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin tx for %s: %w", file, err)
|
||||
}
|
||||
if _, err := tx.Exec(string(content)); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to execute migration %s: %w", file, err)
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO schema_migrations (name) VALUES (?)`, file); err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to record migration %s: %w", file, err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit migration %s: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the database connection.
|
||||
func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// Conn returns the underlying sql.DB connection.
|
||||
func (db *DB) Conn() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
Reference in New Issue
Block a user