diff --git a/.golangci.yml b/.golangci.yml index deb9368..8596adb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,8 +1,6 @@ version: "2" linters: - # consider to add: cyclop - # find sane settings for revive default: none enable: - bodyclose @@ -35,6 +33,7 @@ linters: - paralleltest - perfsprint - prealloc + - revive - staticcheck - tagalign - tagliatelle @@ -63,6 +62,10 @@ linters: json: camel exclusions: rules: + - path-except: "database/*" + linters: + - revive + - path: _test\.go linters: - dupl diff --git a/database/database.go b/database/database.go index 55c1dcd..c55a50f 100644 --- a/database/database.go +++ b/database/database.go @@ -1,3 +1,4 @@ +// Package database provides database connection and migration functionality. package database import ( @@ -5,17 +6,18 @@ import ( "fmt" "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" + _ "github.com/lib/pq" // PostgreSQL driver ) +// Database represents a database connection with migration capabilities. type Database struct { *sqlx.DB repositories map[string]any migrators map[string]migrator - repository *Repository service *service } +// New creates a new Database instance with the given connection string. func New(connection string) (*Database, error) { db, err := sqlx.Connect("postgres", connection) if err != nil { @@ -24,9 +26,11 @@ func New(connection string) (*Database, error) { repository := newRepository(db) service := newService(repository) - return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil + return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), service: service}, nil } +// RegisterRepository registers a repository in the database. +// If repository implements migrator interface, it will migrate when `Migrate` is called. func (db *Database) RegisterRepository(name string, repository any) { db.repositories[name] = repository @@ -35,15 +39,16 @@ func (db *Database) RegisterRepository(name string, repository any) { } } +// Migrate runs all pending migrations for registered repositories. func (db *Database) Migrate(ctx context.Context) error { // Ensure that migration table exists - err := db.service.MigrateSelf(ctx) + err := db.service.migrateSelf(ctx) if err != nil { return err } // Get completed migrations - migrationLogs, err := db.service.GetMigrationLogs(ctx) + migrationLogs, err := db.service.getMigrationLogs(ctx) if err != nil { return fmt.Errorf("failed to select migrations state: %w", err) } @@ -57,7 +62,7 @@ func (db *Database) Migrate(ctx context.Context) error { } } - err = db.service.ApplyMigrations(ctx, migrations, migrationLogs) + err = db.service.applyMigrations(ctx, migrations, migrationLogs) if err != nil { return err } diff --git a/database/database_test.go b/database/database_test.go index c48f80b..c03f2b3 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -4,6 +4,7 @@ import ( "context" "slices" "testing" + "time" "github.com/mishankov/platforma/database" "github.com/testcontainers/testcontainers-go/modules/postgres" @@ -59,7 +60,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("failed to migrate database: %s", err.Error()) } - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -73,8 +74,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository) } - if migrationLogs[0].MigrationId != "init" { - t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId) + if migrationLogs[0].MigrationID != "init" { + t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID) } }) @@ -109,7 +110,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("failed to migrate database: %s", err.Error()) } - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -123,8 +124,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository) } - if migrationLogs[0].MigrationId != "init" { - t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId) + if migrationLogs[0].MigrationID != "init" { + t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID) } }) @@ -156,7 +157,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("failed to migrate database: %s", err.Error()) } - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -167,8 +168,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected 2 migrations, got: %d", len(migrationLogs)) } - if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "some_repo" && log.MigrationId == "init" + if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "some_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to contain init migration for some_repo") } @@ -213,7 +214,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("failed to migrate database: %s", err.Error()) } - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -224,8 +225,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected 3 migrations, got: %d", len(migrationLogs)) } - if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "some_repo" && log.MigrationId == "init" + if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "some_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to contain init migration for some_repo") } @@ -235,8 +236,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected no errors, got: %s", err.Error()) } - if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "other_repo" && log.MigrationId == "init" + if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "other_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to contain init migration for other_repo, but only got: %s", migrationLogs) } @@ -286,7 +287,7 @@ func TestMigrate(t *testing.T) { } t.Logf("migration error: %s", err.Error()) - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -300,13 +301,13 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository) } - if migrationLogs[0].MigrationId != "init" { - t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId) + if migrationLogs[0].MigrationID != "init" { + t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID) } // because migration should be reverted - if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "some_repo" && log.MigrationId == "init" + if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "some_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to not contain init migration for some_repo") } @@ -317,8 +318,8 @@ func TestMigrate(t *testing.T) { } // because migration should be reverted - if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "other_repo" && log.MigrationId == "init" + if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "other_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to not contain init migration for other_repo, but only got: %s", migrationLogs) } @@ -368,7 +369,7 @@ func TestMigrate(t *testing.T) { } t.Logf("migration error: %s", err.Error()) - var migrationLogs []database.MigrationLog + var migrationLogs []migrationLog err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) @@ -382,20 +383,20 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository) } - if migrationLogs[0].MigrationId != "init" { - t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId) + if migrationLogs[0].MigrationID != "init" { + t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID) } // because migration should be reverted or not even attempted - if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "some_repo" && log.MigrationId == "init" + if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "some_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to not contain init migration for some_repo") } // because migration should be reverted - if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { - return log.Repository == "other_repo" && log.MigrationId == "init" + if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool { + return log.Repository == "other_repo" && log.MigrationID == "init" }) { t.Fatalf("expected migration log to not contain init migration for other_repo, but only got: %s", migrationLogs) } @@ -407,6 +408,12 @@ func TestMigrate(t *testing.T) { }) } +type migrationLog struct { + Repository string `db:"repository"` + MigrationID string `db:"id"` + Timestamp time.Time `db:"timestamp"` +} + type simpleRepo struct { migrations []database.Migration } diff --git a/database/migration.go b/database/migration.go index dfe5f19..5d5c40d 100644 --- a/database/migration.go +++ b/database/migration.go @@ -4,12 +4,13 @@ import ( "time" ) -type MigrationLog struct { +type migrationLog struct { Repository string `db:"repository"` - MigrationId string `db:"id"` + MigrationID string `db:"id"` Timestamp time.Time `db:"timestamp"` } +// Migration represents a database migration with up and down SQL statements. type Migration struct { ID string Up string diff --git a/database/repository.go b/database/repository.go index 7ab923d..c846ba1 100644 --- a/database/repository.go +++ b/database/repository.go @@ -7,15 +7,15 @@ import ( "github.com/jmoiron/sqlx" ) -type Repository struct { +type repository struct { db *sqlx.DB } -func newRepository(db *sqlx.DB) *Repository { - return &Repository{db: db} +func newRepository(db *sqlx.DB) *repository { + return &repository{db: db} } -func (r *Repository) Migrations() []Migration { +func (r *repository) migrations() []Migration { return []Migration{{ ID: "init", Up: "CREATE TABLE IF NOT EXISTS platforma_migrations (repository TEXT, id TEXT, timestamp TIMESTAMP)", @@ -23,8 +23,8 @@ func (r *Repository) Migrations() []Migration { }} } -func (r *Repository) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { - var migrations []MigrationLog +func (r *repository) getMigrationLogs(ctx context.Context) ([]migrationLog, error) { + var migrations []migrationLog err := r.db.SelectContext(ctx, &migrations, "SELECT * FROM platforma_migrations") if err != nil { return nil, fmt.Errorf("failed to get migration logs: %w", err) @@ -33,7 +33,7 @@ func (r *Repository) GetMigrationLogs(ctx context.Context) ([]MigrationLog, erro return migrations, nil } -func (r *Repository) SaveMigrationLog(ctx context.Context, log MigrationLog) error { +func (r *repository) saveMigrationLog(ctx context.Context, log migrationLog) error { query := ` INSERT INTO platforma_migrations (repository, id, timestamp) VALUES (:repository, :id, :timestamp) @@ -45,7 +45,7 @@ func (r *Repository) SaveMigrationLog(ctx context.Context, log MigrationLog) err return nil } -func (r *Repository) ExecuteQuery(ctx context.Context, query string) error { +func (r *repository) executeQuery(ctx context.Context, query string) error { _, err := r.db.ExecContext(ctx, query) if err != nil { return fmt.Errorf("failed to execute query: %w", err) diff --git a/database/service.go b/database/service.go index 5fbfa64..5ddfe48 100644 --- a/database/service.go +++ b/database/service.go @@ -10,41 +10,34 @@ import ( "github.com/mishankov/platforma/log" ) -type repository interface { - GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) - SaveMigrationLog(ctx context.Context, log MigrationLog) error - ExecuteQuery(ctx context.Context, query string) error - Migrations() []Migration -} - type service struct { - repo repository + repo *repository } -func newService(repo repository) *service { +func newService(repo *repository) *service { return &service{repo: repo} } -func (s *service) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { - logs, err := s.repo.GetMigrationLogs(ctx) +func (s *service) getMigrationLogs(ctx context.Context) ([]migrationLog, error) { + logs, err := s.repo.getMigrationLogs(ctx) if err != nil { return nil, fmt.Errorf("failed to get migration logs: %w", err) } return logs, nil } -func (s *service) SaveMigrationLog(ctx context.Context, repository, migrationId string) error { - err := s.repo.SaveMigrationLog(ctx, MigrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) +func (s *service) saveMigrationLog(ctx context.Context, repository, migrationID string) error { + err := s.repo.saveMigrationLog(ctx, migrationLog{Repository: repository, MigrationID: migrationID, Timestamp: time.Now()}) if err != nil { return fmt.Errorf("failed to save migration log: %w", err) } return nil } -func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) error { +func (s *service) saveMigrationLogs(ctx context.Context, migrations []Migration) error { masterErr := error(nil) for _, migr := range migrations { - err := s.SaveMigrationLog(ctx, migr.repository, migr.ID) + err := s.saveMigrationLog(ctx, migr.repository, migr.ID) if err != nil { masterErr = errors.Join(masterErr, err) } @@ -53,22 +46,22 @@ func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) return masterErr } -func (s *service) MigrateSelf(ctx context.Context) error { - migrations := s.repo.Migrations() +func (s *service) migrateSelf(ctx context.Context) error { + migrations := s.repo.migrations() appliedMigrations := []Migration{} - migrationLogs, err := s.repo.GetMigrationLogs(ctx) + migrationLogs, err := s.repo.getMigrationLogs(ctx) if err != nil { log.InfoContext(ctx, "migrations log table does not exist yet") } for _, migr := range migrations { - if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { - return l.Repository == "platforma_migration" && l.MigrationId == migr.ID + if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { + return l.Repository == "platforma_migration" && l.MigrationID == migr.ID }) { - err := s.ApplyMigration(ctx, migr) + err := s.applyMigration(ctx, migr) if err != nil { - revertErr := s.RevertMigrations(ctx, appliedMigrations) + revertErr := s.revertMigrations(ctx, appliedMigrations) if revertErr != nil { log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) } @@ -79,7 +72,7 @@ func (s *service) MigrateSelf(ctx context.Context) error { } } - err = s.SaveMigrationLogs(ctx, appliedMigrations) + err = s.saveMigrationLogs(ctx, appliedMigrations) if err != nil { log.ErrorContext(ctx, "got error(s) trying to save migration logs", "error", err.Error()) } @@ -87,23 +80,23 @@ func (s *service) MigrateSelf(ctx context.Context) error { return nil } -func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { - err := s.repo.ExecuteQuery(ctx, migration.Up) +func (s *service) applyMigration(ctx context.Context, migration Migration) error { + err := s.repo.executeQuery(ctx, migration.Up) if err != nil { return fmt.Errorf("failed to apply migration: %w", err) } return nil } -func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, migrationLogs []MigrationLog) error { +func (s *service) applyMigrations(ctx context.Context, migrations []Migration, migrationLogs []migrationLog) error { appliedMigrations := []Migration{} for _, migr := range migrations { - if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { - return l.Repository == migr.repository && l.MigrationId == migr.ID + if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { + return l.Repository == migr.repository && l.MigrationID == migr.ID }) { - err := s.ApplyMigration(ctx, migr) + err := s.applyMigration(ctx, migr) if err != nil { - revertErr := s.RevertMigrations(ctx, appliedMigrations) + revertErr := s.revertMigrations(ctx, appliedMigrations) if revertErr != nil { log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) } @@ -113,7 +106,7 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m } } - err := s.SaveMigrationLogs(ctx, appliedMigrations) + err := s.saveMigrationLogs(ctx, appliedMigrations) if err != nil { log.ErrorContext(ctx, "got error(s) trying to save migration logs", "error", err.Error()) } @@ -121,18 +114,18 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m return nil } -func (s *service) RevertMigration(ctx context.Context, migration Migration) error { - err := s.repo.ExecuteQuery(ctx, migration.Down) +func (s *service) revertMigration(ctx context.Context, migration Migration) error { + err := s.repo.executeQuery(ctx, migration.Down) if err != nil { return fmt.Errorf("failed to revert migration: %w", err) } return nil } -func (s *service) RevertMigrations(ctx context.Context, migrations []Migration) error { +func (s *service) revertMigrations(ctx context.Context, migrations []Migration) error { masterErr := error(nil) for _, migr := range slices.Backward(migrations) { - err := s.RevertMigration(ctx, migr) + err := s.revertMigration(ctx, migr) if err != nil { masterErr = errors.Join(masterErr, fmt.Errorf("failed to revert migration %s: %w", migr.ID, err)) }