package store import ( "context" "database/sql" "embed" "fmt" "sort" "strings" ) //go:embed migrations/*.sql var migrationsFS embed.FS // MigrationVersion returns the name of the highest applied migration // (e.g. "0005_node_capacity.sql"). Returns ("", nil) if no migrations // have been applied (fresh or empty database). func MigrationVersion(ctx context.Context, db *sql.DB) (string, error) { var name string err := db.QueryRowContext(ctx, `SELECT name FROM schema_migrations ORDER BY name DESC LIMIT 1`).Scan(&name) if err == sql.ErrNoRows { return "", nil } if err != nil { return "", fmt.Errorf("query migration version: %w", err) } return name, nil } func migrate(db *sql.DB) error { entries, err := migrationsFS.ReadDir("migrations") if err != nil { return fmt.Errorf("read migrations dir: %w", err) } names := make([]string, 0, len(entries)) for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") { names = append(names, e.Name()) } } sort.Strings(names) if _, err := db.ExecContext(context.Background(), `CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at DATETIME NOT NULL)`); err != nil { return fmt.Errorf("create schema_migrations: %w", err) } for _, name := range names { var existing string err := db.QueryRowContext(context.Background(), `SELECT name FROM schema_migrations WHERE name = ?`, name).Scan(&existing) if err == nil { continue } if err != sql.ErrNoRows { return fmt.Errorf("check migration %s: %w", name, err) } sqlBytes, err := migrationsFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } if _, err := db.ExecContext(context.Background(), string(sqlBytes)); err != nil { return fmt.Errorf("apply migration %s: %w", name, err) } if _, err := db.ExecContext(context.Background(), `INSERT INTO schema_migrations (name, applied_at) VALUES (?, datetime('now'))`, name); err != nil { return fmt.Errorf("record migration %s: %w", name, err) } } return nil }