Files
havline-native/app-src/internal/db/db.go
T

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
}