// File Path: monorepo/cloud/maplefile-backend/pkg/storage/database/cassandradb/migration.go package cassandradb import ( "fmt" "go.uber.org/zap" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/cassandra" _ "github.com/golang-migrate/migrate/v4/source/file" "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config" ) // Migrator handles database schema migrations // This encapsulates all migration logic and makes it testable type Migrator struct { config config.DatabaseConfig logger *zap.Logger } // NewMigrator creates a new migration manager that works with fx dependency injection func NewMigrator(cfg *config.Configuration, logger *zap.Logger) *Migrator { return &Migrator{ config: cfg.Database, logger: logger.Named("Migrator"), } } // Up runs all pending migrations with dirty state recovery func (m *Migrator) Up() error { m.logger.Info("Creating migrator") migrateInstance, err := m.createMigrate() if err != nil { return fmt.Errorf("failed to create migrator: %w", err) } defer migrateInstance.Close() m.logger.Info("Checking migration version") version, dirty, err := migrateInstance.Version() if err != nil && err != migrate.ErrNilVersion { return fmt.Errorf("failed to get migration version: %w", err) } if dirty { m.logger.Warn("Database is in dirty state, attempting to force clean state", zap.Uint("version", version)) if err := migrateInstance.Force(int(version)); err != nil { return fmt.Errorf("failed to force clean migration state: %w", err) } } // Run migrations if err := migrateInstance.Up(); err != nil && err != migrate.ErrNoChange { return fmt.Errorf("failed to run migrations: %w", err) } // Get final version finalVersion, _, err := migrateInstance.Version() if err != nil && err != migrate.ErrNilVersion { m.logger.Warn("Could not get final migration version", zap.Error(err)) } else if err != migrate.ErrNilVersion { m.logger.Info("Database migrations completed successfully", zap.Uint("version", finalVersion)) } else { m.logger.Info("Database migrations completed successfully (no migrations applied)") } return nil } // Down rolls back the last migration // Useful for development and rollback scenarios func (m *Migrator) Down() error { migrate, err := m.createMigrate() if err != nil { return fmt.Errorf("failed to create migrator: %w", err) } defer migrate.Close() if err := migrate.Steps(-1); err != nil { return fmt.Errorf("failed to rollback migration: %w", err) } return nil } // Version returns the current migration version func (m *Migrator) Version() (uint, bool, error) { migrate, err := m.createMigrate() if err != nil { return 0, false, fmt.Errorf("failed to create migrator: %w", err) } defer migrate.Close() return migrate.Version() } // ForceVersion forces the migration version (useful for fixing dirty states) func (m *Migrator) ForceVersion(version int) error { migrateInstance, err := m.createMigrate() if err != nil { return fmt.Errorf("failed to create migrator: %w", err) } defer migrateInstance.Close() if err := migrateInstance.Force(version); err != nil { return fmt.Errorf("failed to force version %d: %w", version, err) } m.logger.Info("Successfully forced migration version", zap.Int("version", version)) return nil } // createMigrate creates a migrate instance with proper configuration func (m *Migrator) createMigrate() (*migrate.Migrate, error) { // Build Cassandra connection string // Format: cassandra://host:port/keyspace?consistency=level databaseURL := fmt.Sprintf("cassandra://%s/%s?consistency=%s", m.config.Hosts[0], // Use first host for migrations m.config.Keyspace, m.config.Consistency, ) // Add authentication if configured if m.config.Username != "" && m.config.Password != "" { databaseURL = fmt.Sprintf("cassandra://%s:%s@%s/%s?consistency=%s", m.config.Username, m.config.Password, m.config.Hosts[0], m.config.Keyspace, m.config.Consistency, ) } // Create migrate instance migrate, err := migrate.New(m.config.MigrationsPath, databaseURL) if err != nil { return nil, fmt.Errorf("failed to initialize migrate: %w", err) } return migrate, nil }