114 lines
2.5 KiB
Go
114 lines
2.5 KiB
Go
package db
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
_ "modernc.org/sqlite"
|
||
|
|
)
|
||
|
|
|
||
|
|
func Open(path string) (*sql.DB, error) {
|
||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)", path)
|
||
|
|
conn, err := sql.Open("sqlite", dsn)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
conn.SetMaxOpenConns(1)
|
||
|
|
return conn, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func Migrate(ctx context.Context, conn *sql.DB, migrationsDir string) error {
|
||
|
|
if err := ensureMigrationsTable(ctx, conn); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
entries, err := os.ReadDir(migrationsDir)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
var files []string
|
||
|
|
for _, e := range entries {
|
||
|
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".sql") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
files = append(files, e.Name())
|
||
|
|
}
|
||
|
|
sort.Strings(files)
|
||
|
|
|
||
|
|
for _, name := range files {
|
||
|
|
version, err := parseVersion(name)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
applied, err := isApplied(ctx, conn, version)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if applied {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
content, err := os.ReadFile(filepath.Join(migrationsDir, name))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
tx, err := conn.BeginTx(ctx, nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := tx.ExecContext(ctx, string(content)); err != nil {
|
||
|
|
_ = tx.Rollback()
|
||
|
|
return fmt.Errorf("migration %s failed: %w", name, err)
|
||
|
|
}
|
||
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO schema_migrations(version) VALUES (?)`, version); err != nil {
|
||
|
|
_ = tx.Rollback()
|
||
|
|
return fmt.Errorf("record migration %s failed: %w", name, err)
|
||
|
|
}
|
||
|
|
if err := tx.Commit(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func ensureMigrationsTable(ctx context.Context, conn *sql.DB) error {
|
||
|
|
_, err := conn.ExecContext(ctx, `
|
||
|
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||
|
|
version INTEGER PRIMARY KEY,
|
||
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
|
|
)
|
||
|
|
`)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseVersion(name string) (int, error) {
|
||
|
|
parts := strings.SplitN(name, "_", 2)
|
||
|
|
if len(parts) == 0 {
|
||
|
|
return 0, fmt.Errorf("invalid migration filename: %s", name)
|
||
|
|
}
|
||
|
|
var version int
|
||
|
|
if _, err := fmt.Sscanf(parts[0], "%d", &version); err != nil {
|
||
|
|
return 0, fmt.Errorf("invalid migration version in %s: %w", name, err)
|
||
|
|
}
|
||
|
|
return version, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func isApplied(ctx context.Context, conn *sql.DB, version int) (bool, error) {
|
||
|
|
var count int
|
||
|
|
err := conn.QueryRowContext(ctx, `SELECT COUNT(1) FROM schema_migrations WHERE version = ?`, version).Scan(&count)
|
||
|
|
return count > 0, err
|
||
|
|
}
|