package store import ( "embed" "fmt" "sort" "strconv" "strings" "time" ) //go:embed migrations/*.sql var migrationFS embed.FS func (s *Store) applyMigrations() error { if _, err := s.db.Exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL ) `); err != nil { return fmt.Errorf("bootstrap schema_migrations: %w", err) } entries, err := migrationFS.ReadDir("migrations") if err != nil { return fmt.Errorf("read migrations: %w", err) } var files []string for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") { continue } files = append(files, entry.Name()) } sort.Strings(files) for _, name := range files { version, err := migrationVersion(name) if err != nil { return err } applied, err := s.isMigrationApplied(version) if err != nil { return err } if applied { continue } sqlBytes, err := migrationFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } if _, err := s.db.Exec(string(sqlBytes)); err != nil { return fmt.Errorf("apply migration %s: %w", name, err) } if _, err := s.db.Exec( `INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, version, time.Now().Unix(), ); err != nil { return fmt.Errorf("record migration %s: %w", name, err) } } return nil } func migrationVersion(name string) (int, error) { prefix := strings.SplitN(name, "_", 2)[0] version, err := strconv.Atoi(prefix) if err != nil { return 0, fmt.Errorf("invalid migration filename %q: %w", name, err) } return version, nil } func (s *Store) isMigrationApplied(version int) (bool, error) { var count int err := s.db.QueryRow( `SELECT COUNT(*) FROM schema_migrations WHERE version = ?`, version, ).Scan(&count) if err != nil { return false, err } return count > 0, nil }