Run executes all collected migrations sorted by filename. Uses a migrations tracking table to avoid re-running.
()
| 66 | // Run executes all collected migrations sorted by filename. |
| 67 | // Uses a migrations tracking table to avoid re-running. |
| 68 | func (m *Migrator) Run() error { |
| 69 | // Create migrations tracking table. |
| 70 | if _, err := m.db.Exec(`CREATE TABLE IF NOT EXISTS migrations ( |
| 71 | name TEXT PRIMARY KEY, |
| 72 | module TEXT NOT NULL, |
| 73 | applied_at DATETIME DEFAULT CURRENT_TIMESTAMP |
| 74 | )`); err != nil { |
| 75 | return fmt.Errorf("create migrations table: %w", err) |
| 76 | } |
| 77 | |
| 78 | // Sort all migrations by name (date-based naming ensures correct order). |
| 79 | sort.Slice(m.migrations, func(i, j int) bool { |
| 80 | return m.migrations[i].Name < m.migrations[j].Name |
| 81 | }) |
| 82 | |
| 83 | // Run each migration. |
| 84 | for _, mig := range m.migrations { |
| 85 | // Check if already applied. |
| 86 | var count int |
| 87 | m.db.QueryRow(`SELECT COUNT(*) FROM migrations WHERE name = ?`, mig.Module+"/"+mig.Name).Scan(&count) |
| 88 | if count > 0 { |
| 89 | continue |
| 90 | } |
| 91 | |
| 92 | slog.Info("running migration", "module", mig.Module, "name", mig.Name) |
| 93 | |
| 94 | // Execute all statements in the migration file. |
| 95 | stmts := splitStatements(mig.Content) |
| 96 | for _, stmt := range stmts { |
| 97 | stmt = strings.TrimSpace(stmt) |
| 98 | if stmt == "" { |
| 99 | continue |
| 100 | } |
| 101 | if _, err := m.db.Exec(stmt); err != nil { |
| 102 | return fmt.Errorf("migration %s/%s failed: %w\nSQL: %s", mig.Module, mig.Name, err, stmt) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // Mark as applied. |
| 107 | if _, err := m.db.Exec( |
| 108 | `INSERT INTO migrations (name, module) VALUES (?, ?)`, |
| 109 | mig.Module+"/"+mig.Name, mig.Module, |
| 110 | ); err != nil { |
| 111 | return fmt.Errorf("record migration %s/%s: %w", mig.Module, mig.Name, err) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | return nil |
| 116 | } |
| 117 | |
| 118 | // splitStatements splits SQL content by semicolons, respecting basic quoting. |
| 119 | func splitStatements(sql string) []string { |