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 }