From 9872210b6cd48c331372476c272bd0bf14dbab24 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 15:27:11 +0300 Subject: [PATCH 01/35] fix(database): correct interface name from shemer to schemer --- database/database.go | 6 +++--- database/migration.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/database/database.go b/database/database.go index 17b23d5..50af345 100644 --- a/database/database.go +++ b/database/database.go @@ -15,7 +15,7 @@ import ( type Database struct { *sqlx.DB repositories map[string]any - migrators map[string]shemer + migrators map[string]schemer } func New(connection string) (*Database, error) { @@ -23,13 +23,13 @@ func New(connection string) (*Database, error) { if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } - return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]shemer)}, nil + return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]schemer)}, nil } func (db *Database) RegisterRepository(name string, repository any) { db.repositories[name] = repository - if migr, ok := repository.(shemer); ok { + if migr, ok := repository.(schemer); ok { db.migrators[name] = migr } } diff --git a/database/migration.go b/database/migration.go index 8e36510..8e67d9e 100644 --- a/database/migration.go +++ b/database/migration.go @@ -22,6 +22,6 @@ type Schema struct { Queries []string } -type shemer interface { +type schemer interface { Schema() ([]Migration, Schema) } From 0a64e506ec698372566b9b18f915dcd7302e3d1e Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 15:38:32 +0300 Subject: [PATCH 02/35] feat(database): add repository layer for migration management --- database/migration.go | 1 + database/repository.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 database/repository.go diff --git a/database/migration.go b/database/migration.go index 8e67d9e..8d86765 100644 --- a/database/migration.go +++ b/database/migration.go @@ -5,6 +5,7 @@ import ( "time" ) +// TODO: rename to migration type migrations struct { Repository string `db:"repository"` MigrationId sql.NullString `db:"id"` diff --git a/database/repository.go b/database/repository.go new file mode 100644 index 0000000..dbd78d2 --- /dev/null +++ b/database/repository.go @@ -0,0 +1,40 @@ +package database + +import ( + "context" + + "github.com/jmoiron/sqlx" +) + +type repository struct { + db *sqlx.DB +} + +func newRepository(db *sqlx.DB) *repository { + return &repository{db: db} +} + +func (r *repository) Schema() ([]Migration, Schema) { + return []Migration{}, Schema{Queries: []string{ + "CREATE TABLE IF NOT EXISTS platforma_migrations (repository TEXT, id TEXT, timestamp TIMESTAMP)", + }} +} + +func (r *repository) GetMigrations(ctx context.Context) ([]*migrations, error) { + var migrations []*migrations + err := r.db.SelectContext(ctx, &migrations, "SELECT * FROM platforma_migrations") + if err != nil { + return nil, err + } + + return migrations, nil +} + +func (r *repository) SaveMigration(ctx context.Context, migration migrations) error { + query := ` + INSERT INTO platforma_migrations (repository, id, timestamp) + VALUES (:repository, :id, :timestamp) + ` + _, err := r.db.NamedExecContext(ctx, query, migration) + return err +} From 98b5ef12dc0843e9b9d01d1028e8633aa5a533e9 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 15:43:08 +0300 Subject: [PATCH 03/35] feat(database): add service layer for migration management --- database/service.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 database/service.go diff --git a/database/service.go b/database/service.go new file mode 100644 index 0000000..2bc3550 --- /dev/null +++ b/database/service.go @@ -0,0 +1,24 @@ +package database + +import ( + "context" + + "github.com/jmoiron/sqlx" +) + +type service struct { + repo *repository + db *sqlx.DB +} + +func newService(repo *repository, db *sqlx.DB) *service { + return &service{repo: repo, db: db} +} + +func (s *service) GetMigrations(ctx context.Context) ([]*migrations, error) { + return s.repo.GetMigrations(ctx) +} + +func (s *service) SaveMigration(ctx context.Context, migration migrations) error { + return s.repo.SaveMigration(ctx, migration) +} From 3ed44363cd8f74d17b854e95c2f310e7a0fa77cc Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 20:04:34 +0300 Subject: [PATCH 04/35] refactor(database): extract migration logic to service layer --- database/database.go | 58 ++++++++++++++++++++++++++---------------- database/migration.go | 3 +-- database/repository.go | 14 +++++++--- database/service.go | 33 +++++++++++++++++++++--- 4 files changed, 76 insertions(+), 32 deletions(-) diff --git a/database/database.go b/database/database.go index 50af345..f735227 100644 --- a/database/database.go +++ b/database/database.go @@ -2,6 +2,7 @@ package database import ( "context" + "database/sql" "fmt" "slices" "time" @@ -16,6 +17,8 @@ type Database struct { *sqlx.DB repositories map[string]any migrators map[string]schemer + repository *repository + service *service } func New(connection string) (*Database, error) { @@ -23,7 +26,10 @@ func New(connection string) (*Database, error) { if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } - return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]schemer)}, nil + + repository := newRepository(db) + service := newService(repository, db) + return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]schemer), repository: repository, service: service}, nil } func (db *Database) RegisterRepository(name string, repository any) { @@ -35,13 +41,15 @@ func (db *Database) RegisterRepository(name string, repository any) { } func (db *Database) Migrate(ctx context.Context) error { - if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS platforma_migrations (repository TEXT, id TEXT, timestamp TIMESTAMP)"); err != nil { + // Ensure that migration table exists + _, databaseRepoSchema := db.repository.Schema() + err := db.service.ApplySchema(ctx, databaseRepoSchema) + if err != nil { return fmt.Errorf("failed to create migrations table: %w", err) } - // Select data from platforma_migrations table - var migrationsState []migrations - err := db.SelectContext(ctx, &migrationsState, "SELECT * FROM platforma_migrations") + // Get completed migrations + migrationsState, err := db.service.GetMigrationLogs(ctx) if err != nil { return fmt.Errorf("failed to select migrations state: %w", err) } @@ -52,28 +60,28 @@ func (db *Database) Migrate(ctx context.Context) error { for repoName, migr := range db.migrators { repoMigrations, repoSchema := migr.Schema() - repoHasMigrations := slices.ContainsFunc(migrationsState, func(m migrations) bool { + repoHasMigrations := slices.ContainsFunc(migrationsState, func(m migrationLog) bool { return m.Repository == repoName }) // If repo does not has migrations apply schema and exit if !repoHasMigrations { - for _, query := range repoSchema.Queries { - if _, err := db.ExecContext(ctx, query); err != nil { - migrationErr = fmt.Errorf("failed to execute schema query: %w", err) - break - } - log.InfoContext(ctx, "schema applied", "repository", repoName) + err := db.service.ApplySchema(ctx, repoSchema) + if err != nil { + return fmt.Errorf("failed to execute schema query: %w", err) } + log.InfoContext(ctx, "schema applied", "repository", repoName) // Log that schema applied - if _, err := db.ExecContext(ctx, "INSERT INTO platforma_migrations (repository, timestamp) VALUES ($1, $2)", repoName, time.Now()); err != nil { + err = db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, Timestamp: time.Now()}) + if err != nil { return fmt.Errorf("failed to insert migration record: %w", err) } // If schema is applied, log that all migrations are also applied for _, migration := range repoMigrations { - if _, err := db.ExecContext(ctx, "INSERT INTO platforma_migrations (repository, id, timestamp) VALUES ($1, $2, $3)", repoName, migration.ID, time.Now()); err != nil { + err := db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, MigrationId: sql.NullString{String: migration.ID}, Timestamp: time.Now()}) + if err != nil { return fmt.Errorf("failed to insert migration record: %w", err) } } @@ -85,26 +93,31 @@ func (db *Database) Migrate(ctx context.Context) error { migration.repository = repoName // Check if migration has been applied - migrationHasApplied := slices.ContainsFunc(migrationsState, func(m migrations) bool { + migrationHasApplied := slices.ContainsFunc(migrationsState, func(m migrationLog) bool { return m.Repository == repoName && m.MigrationId.String == migration.ID }) + // Skip to next migration if current is applied if migrationHasApplied { continue } - if _, err := db.ExecContext(ctx, migration.Up); err != nil { + // Try to apply mifration + err := db.service.ApplyMigration(ctx, migration) + if err != nil { migrationErr = fmt.Errorf("failed to apply migration %s for repository %s: %w", migration.ID, repoName, err) - log.ErrorContext(ctx, "failed to apply migration for repository", "migration", migration.ID, "repository", repoName) break } + // If migration applied successfuly add it to applied migrations list appliedMigrations = append(appliedMigrations, migration) log.InfoContext(ctx, "applied migration for repository", "migration", migration.ID, "repository", repoName) // Log that migration applied - if _, err := db.ExecContext(ctx, "INSERT INTO platforma_migrations (repository, id, timestamp) VALUES ($1, $2, $3)", repoName, migration.ID, time.Now()); err != nil { - return fmt.Errorf("failed to insert migration record: %w", err) + err = db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, MigrationId: sql.NullString{String: migration.ID}, Timestamp: time.Now()}) + if err != nil { + migrationErr = fmt.Errorf("failed to insert migration record: %w", err) + break } } @@ -115,12 +128,13 @@ func (db *Database) Migrate(ctx context.Context) error { if migrationErr != nil { for _, migration := range slices.Backward(appliedMigrations) { - if _, err := db.ExecContext(ctx, migration.Down); err != nil { - log.ErrorContext(ctx, "failed to rollback migration %s for repository %s", migration.ID, migration.repository) + err := db.service.RevertMigration(ctx, migration) + if err != nil { return fmt.Errorf("failed to rollback migration %s for repository %s: %w", migration.ID, migration.repository, err) } - if _, err := db.ExecContext(ctx, "DELETE FROM platforma_migrations WHERE repository = $1 AND id = $2", migration.repository, migration.ID); err != nil { + err = db.service.RemoveMigrationLog(ctx, migration.repository, migration.ID) + if err != nil { return fmt.Errorf("failed to delete migration record: %w", err) } } diff --git a/database/migration.go b/database/migration.go index 8d86765..25d9dd3 100644 --- a/database/migration.go +++ b/database/migration.go @@ -5,8 +5,7 @@ import ( "time" ) -// TODO: rename to migration -type migrations struct { +type migrationLog struct { Repository string `db:"repository"` MigrationId sql.NullString `db:"id"` Timestamp time.Time `db:"timestamp"` diff --git a/database/repository.go b/database/repository.go index dbd78d2..6ed7576 100644 --- a/database/repository.go +++ b/database/repository.go @@ -20,8 +20,8 @@ func (r *repository) Schema() ([]Migration, Schema) { }} } -func (r *repository) GetMigrations(ctx context.Context) ([]*migrations, error) { - var migrations []*migrations +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, err @@ -30,11 +30,17 @@ func (r *repository) GetMigrations(ctx context.Context) ([]*migrations, error) { return migrations, nil } -func (r *repository) SaveMigration(ctx context.Context, migration migrations) error { +func (r *repository) SaveMigrationLog(ctx context.Context, log migrationLog) error { query := ` INSERT INTO platforma_migrations (repository, id, timestamp) VALUES (:repository, :id, :timestamp) ` - _, err := r.db.NamedExecContext(ctx, query, migration) + _, err := r.db.NamedExecContext(ctx, query, log) + return err +} + +func (r *repository) RemoveMigrationLog(ctx context.Context, repository, id string) error { + query := `DELETE FROM platforma_migrations WHERE repository = $1 AND id = $2` + _, err := r.db.ExecContext(ctx, query, repository, id) return err } diff --git a/database/service.go b/database/service.go index 2bc3550..5ab36ef 100644 --- a/database/service.go +++ b/database/service.go @@ -15,10 +15,35 @@ func newService(repo *repository, db *sqlx.DB) *service { return &service{repo: repo, db: db} } -func (s *service) GetMigrations(ctx context.Context) ([]*migrations, error) { - return s.repo.GetMigrations(ctx) +func (s *service) GetMigrationLogs(ctx context.Context) ([]migrationLog, error) { + return s.repo.GetMigrationLogs(ctx) } -func (s *service) SaveMigration(ctx context.Context, migration migrations) error { - return s.repo.SaveMigration(ctx, migration) +func (s *service) SaveMigrationLog(ctx context.Context, migration migrationLog) error { + return s.repo.SaveMigrationLog(ctx, migration) +} + +func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) error { + return s.repo.RemoveMigrationLog(ctx, repository, id) +} + +func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { + _, err := s.db.ExecContext(ctx, migration.Up) + return err +} + +func (s *service) RevertMigration(ctx context.Context, migration Migration) error { + _, err := s.db.ExecContext(ctx, migration.Down) + return err +} + +func (s *service) ApplySchema(ctx context.Context, schema Schema) error { + for _, query := range schema.Queries { + _, err := s.db.ExecContext(ctx, query) + if err != nil { + return err + } + } + + return nil } From 6ede75db6d49bb8530a9ec731ace51f038a51e08 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 20:09:31 +0300 Subject: [PATCH 05/35] feat(tooling): add taskfile for golangci-lint integration --- Taskfile.dist.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Taskfile.dist.yml diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml new file mode 100644 index 0000000..4cf7ad0 --- /dev/null +++ b/Taskfile.dist.yml @@ -0,0 +1,10 @@ +version: '3' + +tasks: + lint: + cmds: + - golangci-lint run + + fix: + cmds: + - golangci-lint run --fix From a33618f7acaace2aa75acb62d5626803e33a4c61 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 20:09:45 +0300 Subject: [PATCH 06/35] feat(database): improve error handling in migration operations --- database/repository.go | 13 ++++++++++--- database/service.go | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/database/repository.go b/database/repository.go index 6ed7576..4560ef5 100644 --- a/database/repository.go +++ b/database/repository.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" "github.com/jmoiron/sqlx" ) @@ -24,7 +25,7 @@ func (r *repository) GetMigrationLogs(ctx context.Context) ([]migrationLog, erro var migrations []migrationLog err := r.db.SelectContext(ctx, &migrations, "SELECT * FROM platforma_migrations") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get migration logs: %w", err) } return migrations, nil @@ -36,11 +37,17 @@ func (r *repository) SaveMigrationLog(ctx context.Context, log migrationLog) err VALUES (:repository, :id, :timestamp) ` _, err := r.db.NamedExecContext(ctx, query, log) - return err + if err != nil { + return fmt.Errorf("failed to save migration log: %w", err) + } + return nil } func (r *repository) RemoveMigrationLog(ctx context.Context, repository, id string) error { query := `DELETE FROM platforma_migrations WHERE repository = $1 AND id = $2` _, err := r.db.ExecContext(ctx, query, repository, id) - return err + if err != nil { + return fmt.Errorf("failed to remove migration log: %w", err) + } + return nil } diff --git a/database/service.go b/database/service.go index 5ab36ef..f9a4482 100644 --- a/database/service.go +++ b/database/service.go @@ -2,6 +2,7 @@ package database import ( "context" + "fmt" "github.com/jmoiron/sqlx" ) @@ -29,19 +30,25 @@ func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { _, err := s.db.ExecContext(ctx, migration.Up) - return err + if err != nil { + return fmt.Errorf("failed to apply migration: %w", err) + } + return nil } func (s *service) RevertMigration(ctx context.Context, migration Migration) error { _, err := s.db.ExecContext(ctx, migration.Down) - return err + if err != nil { + return fmt.Errorf("failed to revert migration: %w", err) + } + return nil } func (s *service) ApplySchema(ctx context.Context, schema Schema) error { for _, query := range schema.Queries { _, err := s.db.ExecContext(ctx, query) if err != nil { - return err + return fmt.Errorf("failed to apply schema query: %w", err) } } From ec812392c3aaa0be47fdff6e2ef7db37b8b5d4f4 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Sun, 26 Oct 2025 20:29:46 +0300 Subject: [PATCH 07/35] feat(tooling): add test task to taskfile --- Taskfile.dist.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml index 4cf7ad0..d9a0a01 100644 --- a/Taskfile.dist.yml +++ b/Taskfile.dist.yml @@ -8,3 +8,7 @@ tasks: fix: cmds: - golangci-lint run --fix + + test: + cmds: + - go test -cover ./... From 811e6e1b5817bd8e804abcaf5fe7256fdf90fb8e Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 17:31:15 +0300 Subject: [PATCH 08/35] refactor(database): migrations without schemas --- auth/repository.go | 31 ++++++------ database/database.go | 110 ++++++----------------------------------- database/migration.go | 8 +-- database/repository.go | 8 +-- database/service.go | 62 ++++++++++++++++++++--- session/repository.go | 16 +++--- 6 files changed, 101 insertions(+), 134 deletions(-) diff --git a/auth/repository.go b/auth/repository.go index 293ef19..4ead465 100644 --- a/auth/repository.go +++ b/auth/repository.go @@ -25,22 +25,21 @@ func NewRepository(db db) *Repository { } } -func (r *Repository) Schema() ([]database.Migration, database.Schema) { - return []database.Migration{}, database.Schema{ - Queries: []string{ - ` - CREATE TABLE IF NOT EXISTS users ( - id VARCHAR(255) PRIMARY KEY, - username VARCHAR(255) UNIQUE, - password TEXT, - salt TEXT, - created TIMESTAMP, - updated TIMESTAMP, - status VARCHAR(50) - ) - `, - }, - } +func (r *Repository) Schema() []database.Migration { + return []database.Migration{{ + ID: "init", + Up: `CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(255) PRIMARY KEY, + username VARCHAR(255) UNIQUE, + password TEXT, + salt TEXT, + created TIMESTAMP, + updated TIMESTAMP, + status VARCHAR(50) + )`, + Down: "DROP TABLE users", + }} + } func (r *Repository) Get(ctx context.Context, id string) (*User, error) { diff --git a/database/database.go b/database/database.go index f735227..7537f21 100644 --- a/database/database.go +++ b/database/database.go @@ -2,12 +2,7 @@ package database import ( "context" - "database/sql" "fmt" - "slices" - "time" - - "github.com/mishankov/platforma/log" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -16,7 +11,7 @@ import ( type Database struct { *sqlx.DB repositories map[string]any - migrators map[string]schemer + migrators map[string]migrator repository *repository service *service } @@ -29,116 +24,43 @@ func New(connection string) (*Database, error) { repository := newRepository(db) service := newService(repository, db) - return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]schemer), repository: repository, service: service}, nil + return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil } func (db *Database) RegisterRepository(name string, repository any) { db.repositories[name] = repository - if migr, ok := repository.(schemer); ok { + if migr, ok := repository.(migrator); ok { db.migrators[name] = migr } } func (db *Database) Migrate(ctx context.Context) error { // Ensure that migration table exists - _, databaseRepoSchema := db.repository.Schema() - err := db.service.ApplySchema(ctx, databaseRepoSchema) + err := db.service.MigrateSelf(ctx) if err != nil { - return fmt.Errorf("failed to create migrations table: %w", err) + return err } // Get completed migrations - migrationsState, err := db.service.GetMigrationLogs(ctx) + migrationLogs, err := db.service.GetMigrationLogs(ctx) if err != nil { return fmt.Errorf("failed to select migrations state: %w", err) } - appliedMigrations := []Migration{} - migrationErr := error(nil) - - for repoName, migr := range db.migrators { - repoMigrations, repoSchema := migr.Schema() - - repoHasMigrations := slices.ContainsFunc(migrationsState, func(m migrationLog) bool { - return m.Repository == repoName - }) - - // If repo does not has migrations apply schema and exit - if !repoHasMigrations { - err := db.service.ApplySchema(ctx, repoSchema) - if err != nil { - return fmt.Errorf("failed to execute schema query: %w", err) - } - log.InfoContext(ctx, "schema applied", "repository", repoName) - - // Log that schema applied - err = db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, Timestamp: time.Now()}) - if err != nil { - return fmt.Errorf("failed to insert migration record: %w", err) - } - - // If schema is applied, log that all migrations are also applied - for _, migration := range repoMigrations { - err := db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, MigrationId: sql.NullString{String: migration.ID}, Timestamp: time.Now()}) - if err != nil { - return fmt.Errorf("failed to insert migration record: %w", err) - } - } - - continue - } - - for _, migration := range repoMigrations { - migration.repository = repoName - - // Check if migration has been applied - migrationHasApplied := slices.ContainsFunc(migrationsState, func(m migrationLog) bool { - return m.Repository == repoName && m.MigrationId.String == migration.ID - }) - - // Skip to next migration if current is applied - if migrationHasApplied { - continue - } - - // Try to apply mifration - err := db.service.ApplyMigration(ctx, migration) - if err != nil { - migrationErr = fmt.Errorf("failed to apply migration %s for repository %s: %w", migration.ID, repoName, err) - break - } - - // If migration applied successfuly add it to applied migrations list - appliedMigrations = append(appliedMigrations, migration) - log.InfoContext(ctx, "applied migration for repository", "migration", migration.ID, "repository", repoName) - - // Log that migration applied - err = db.service.SaveMigrationLog(ctx, migrationLog{Repository: repoName, MigrationId: sql.NullString{String: migration.ID}, Timestamp: time.Now()}) - if err != nil { - migrationErr = fmt.Errorf("failed to insert migration record: %w", err) - break - } - } - - if migrationErr != nil { - break + // Get migrations from all migrators + migrations := []Migration{} + for name, migrator := range db.migrators { + for _, migr := range migrator.Migrations() { + migr.repository = name + migrations = append(migrations, migr) } } - if migrationErr != nil { - for _, migration := range slices.Backward(appliedMigrations) { - err := db.service.RevertMigration(ctx, migration) - if err != nil { - return fmt.Errorf("failed to rollback migration %s for repository %s: %w", migration.ID, migration.repository, err) - } - - err = db.service.RemoveMigrationLog(ctx, migration.repository, migration.ID) - if err != nil { - return fmt.Errorf("failed to delete migration record: %w", err) - } - } + err = db.service.ApplyMigrations(ctx, migrations, migrationLogs) + if err != nil { + return err } - return migrationErr + return nil } diff --git a/database/migration.go b/database/migration.go index 25d9dd3..f8b0373 100644 --- a/database/migration.go +++ b/database/migration.go @@ -18,10 +18,6 @@ type Migration struct { repository string } -type Schema struct { - Queries []string -} - -type schemer interface { - Schema() ([]Migration, Schema) +type migrator interface { + Migrations() []Migration } diff --git a/database/repository.go b/database/repository.go index 4560ef5..34a09ff 100644 --- a/database/repository.go +++ b/database/repository.go @@ -15,9 +15,11 @@ func newRepository(db *sqlx.DB) *repository { return &repository{db: db} } -func (r *repository) Schema() ([]Migration, Schema) { - return []Migration{}, Schema{Queries: []string{ - "CREATE TABLE IF NOT EXISTS platforma_migrations (repository TEXT, id TEXT, timestamp TIMESTAMP)", +func (r *repository) Migrations() []Migration { + return []Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS platforma_migrations (repository TEXT, id TEXT, timestamp TIMESTAMP)", + Down: "DROP TABLE platforma_migrations", }} } diff --git a/database/service.go b/database/service.go index f9a4482..f7c6ea7 100644 --- a/database/service.go +++ b/database/service.go @@ -3,8 +3,10 @@ package database import ( "context" "fmt" + "slices" "github.com/jmoiron/sqlx" + "github.com/mishankov/platforma/log" ) type service struct { @@ -28,6 +30,39 @@ func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) return s.repo.RemoveMigrationLog(ctx, repository, id) } +func (s *service) MigrateSelf(ctx context.Context) error { + migrations := s.repo.Migrations() + appliedMigrations := []Migration{} + migrationLogs, err := s.repo.GetMigrationLogs(ctx) + + // If GetMigrationLogs returns error, log table probably does not exist, + // so we should apply all migrations for it + if err != nil { + for _, migr := range migrations { + err := s.ApplyMigration(ctx, migr) + if err != nil { + s.RevertMigrations(ctx, appliedMigrations) + return err + } + appliedMigrations = append(appliedMigrations, migr) + } + } + + for _, migr := range migrations { + if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { + return l.Repository == "platforma_migrations" && l.MigrationId.String == migr.ID + }) { + err := s.ApplyMigration(ctx, migr) + if err != nil { + s.RevertMigrations(ctx, appliedMigrations) + return err + } + } + } + + return nil +} + func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { _, err := s.db.ExecContext(ctx, migration.Up) if err != nil { @@ -36,6 +71,23 @@ func (s *service) ApplyMigration(ctx context.Context, migration Migration) error return nil } +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.String == migr.ID + }) { + err := s.ApplyMigration(ctx, migr) + if err != nil { + s.RevertMigrations(ctx, appliedMigrations) + return err + } + } + } + + return nil +} + func (s *service) RevertMigration(ctx context.Context, migration Migration) error { _, err := s.db.ExecContext(ctx, migration.Down) if err != nil { @@ -44,13 +96,11 @@ func (s *service) RevertMigration(ctx context.Context, migration Migration) erro return nil } -func (s *service) ApplySchema(ctx context.Context, schema Schema) error { - for _, query := range schema.Queries { - _, err := s.db.ExecContext(ctx, query) +func (s *service) RevertMigrations(ctx context.Context, migrations []Migration) { + for _, migr := range slices.Backward(migrations) { + err := s.RevertMigration(ctx, migr) if err != nil { - return fmt.Errorf("failed to apply schema query: %w", err) + log.ErrorContext(ctx, "failed to revert migration", "migration", migr.ID, "error", err.Error()) } } - - return nil } diff --git a/session/repository.go b/session/repository.go index c8bfc9d..87d3e20 100644 --- a/session/repository.go +++ b/session/repository.go @@ -25,19 +25,17 @@ func NewRepository(db db) *Repository { } } -func (r *Repository) Schema() ([]database.Migration, database.Schema) { - return []database.Migration{}, database.Schema{ - Queries: []string{ - ` - CREATE TABLE IF NOT EXISTS sessions ( +func (r *Repository) Schema() []database.Migration { + return []database.Migration{{ + ID: "init", + Up: `CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(255) PRIMARY KEY, "user" VARCHAR(255), created TIMESTAMP, expires TIMESTAMP - ) - `, - }, - } + )`, + Down: "DROP TABLE sessions", + }} } func (r *Repository) Get(ctx context.Context, id string) (*Session, error) { From 60dd5c897d950aabd3d969cc3070c2c42b56a73b Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 19:31:13 +0300 Subject: [PATCH 09/35] create and use SaveMigrationLogs --- database/migration.go | 7 +++---- database/service.go | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/database/migration.go b/database/migration.go index f8b0373..574adcf 100644 --- a/database/migration.go +++ b/database/migration.go @@ -1,14 +1,13 @@ package database import ( - "database/sql" "time" ) type migrationLog struct { - Repository string `db:"repository"` - MigrationId sql.NullString `db:"id"` - Timestamp time.Time `db:"timestamp"` + Repository string `db:"repository"` + MigrationId string `db:"id"` + Timestamp time.Time `db:"timestamp"` } type Migration struct { diff --git a/database/service.go b/database/service.go index f7c6ea7..df330c0 100644 --- a/database/service.go +++ b/database/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "slices" + "time" "github.com/jmoiron/sqlx" "github.com/mishankov/platforma/log" @@ -22,8 +23,17 @@ func (s *service) GetMigrationLogs(ctx context.Context) ([]migrationLog, error) return s.repo.GetMigrationLogs(ctx) } -func (s *service) SaveMigrationLog(ctx context.Context, migration migrationLog) error { - return s.repo.SaveMigrationLog(ctx, migration) +func (s *service) SaveMigrationLog(ctx context.Context, repository, migrationId string) error { + return s.repo.SaveMigrationLog(ctx, migrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) +} + +func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) { + for _, migr := range migrations { + err := s.SaveMigrationLog(ctx, migr.repository, migr.ID) + if err != nil { + log.ErrorContext(ctx, "failed to save migration log", "error", err.Error()) + } + } } func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) error { @@ -50,16 +60,19 @@ func (s *service) MigrateSelf(ctx context.Context) error { for _, migr := range migrations { if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { - return l.Repository == "platforma_migrations" && l.MigrationId.String == migr.ID + return l.Repository == "platforma_migrations" && l.MigrationId == migr.ID }) { err := s.ApplyMigration(ctx, migr) if err != nil { s.RevertMigrations(ctx, appliedMigrations) return err } + appliedMigrations = append(appliedMigrations, migr) } } + s.SaveMigrationLogs(ctx, appliedMigrations) + return nil } @@ -75,7 +88,7 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m appliedMigrations := []Migration{} for _, migr := range migrations { if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { - return l.Repository == migr.repository && l.MigrationId.String == migr.ID + return l.Repository == migr.repository && l.MigrationId == migr.ID }) { err := s.ApplyMigration(ctx, migr) if err != nil { @@ -85,6 +98,8 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m } } + s.SaveMigrationLogs(ctx, appliedMigrations) + return nil } From d5e3f912d2b793a8ea6489597b72f53bbebb6ccb Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:10:14 +0300 Subject: [PATCH 10/35] decouple service and repository --- database/database.go | 2 +- database/repository.go | 14 +++++++------- database/service.go | 11 +++++++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/database/database.go b/database/database.go index 7537f21..70e60c6 100644 --- a/database/database.go +++ b/database/database.go @@ -12,7 +12,7 @@ type Database struct { *sqlx.DB repositories map[string]any migrators map[string]migrator - repository *repository + repository *Repository service *service } diff --git a/database/repository.go b/database/repository.go index 34a09ff..2439d1b 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,7 +23,7 @@ func (r *repository) Migrations() []Migration { }} } -func (r *repository) GetMigrationLogs(ctx context.Context) ([]migrationLog, error) { +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 { @@ -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) RemoveMigrationLog(ctx context.Context, repository, id string) error { +func (r *Repository) RemoveMigrationLog(ctx context.Context, repository, id string) error { query := `DELETE FROM platforma_migrations WHERE repository = $1 AND id = $2` _, err := r.db.ExecContext(ctx, query, repository, id) if err != nil { diff --git a/database/service.go b/database/service.go index df330c0..b861607 100644 --- a/database/service.go +++ b/database/service.go @@ -10,12 +10,19 @@ import ( "github.com/mishankov/platforma/log" ) +type repository interface { + GetMigrationLogs(ctx context.Context) ([]migrationLog, error) + SaveMigrationLog(ctx context.Context, log migrationLog) error + RemoveMigrationLog(ctx context.Context, repository, id string) error + Migrations() []Migration +} + type service struct { - repo *repository + repo repository db *sqlx.DB } -func newService(repo *repository, db *sqlx.DB) *service { +func newService(repo repository, db *sqlx.DB) *service { return &service{repo: repo, db: db} } From 0ddfe7da29568f4ce6cfd59fa45683367bbcf022 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:23:12 +0300 Subject: [PATCH 11/35] first test written --- database/database.go | 2 +- database/migration.go | 2 +- database/repository.go | 11 +++++++--- database/service.go | 31 +++++++++++++++++------------ database/service_test.go | 43 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 database/service_test.go diff --git a/database/database.go b/database/database.go index 70e60c6..c0c97f0 100644 --- a/database/database.go +++ b/database/database.go @@ -23,7 +23,7 @@ func New(connection string) (*Database, error) { } repository := newRepository(db) - service := newService(repository, db) + service := NewService(repository, db) return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil } diff --git a/database/migration.go b/database/migration.go index 574adcf..dfe5f19 100644 --- a/database/migration.go +++ b/database/migration.go @@ -4,7 +4,7 @@ import ( "time" ) -type migrationLog struct { +type MigrationLog struct { Repository string `db:"repository"` MigrationId string `db:"id"` Timestamp time.Time `db:"timestamp"` diff --git a/database/repository.go b/database/repository.go index 2439d1b..3fcf05d 100644 --- a/database/repository.go +++ b/database/repository.go @@ -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) @@ -53,3 +53,8 @@ func (r *Repository) RemoveMigrationLog(ctx context.Context, repository, id stri } return nil } + +func (r *Repository) ExecuteQuery(ctx context.Context, query string) error { + _, err := r.db.ExecContext(ctx, query) + return err +} diff --git a/database/service.go b/database/service.go index b861607..675ce5e 100644 --- a/database/service.go +++ b/database/service.go @@ -2,6 +2,7 @@ package database import ( "context" + "errors" "fmt" "slices" "time" @@ -11,36 +12,40 @@ import ( ) type repository interface { - GetMigrationLogs(ctx context.Context) ([]migrationLog, error) - SaveMigrationLog(ctx context.Context, log migrationLog) error + GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) + SaveMigrationLog(ctx context.Context, log MigrationLog) error RemoveMigrationLog(ctx context.Context, repository, id string) error + ExecuteQuery(ctx context.Context, query string) error Migrations() []Migration } type service struct { repo repository - db *sqlx.DB } -func newService(repo repository, db *sqlx.DB) *service { - return &service{repo: repo, db: db} +func NewService(repo repository, db *sqlx.DB) *service { + return &service{repo: repo} } -func (s *service) GetMigrationLogs(ctx context.Context) ([]migrationLog, error) { +func (s *service) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { return s.repo.GetMigrationLogs(ctx) } func (s *service) SaveMigrationLog(ctx context.Context, repository, migrationId string) error { - return s.repo.SaveMigrationLog(ctx, migrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) + return s.repo.SaveMigrationLog(ctx, MigrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) } -func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) { +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) if err != nil { + errors.Join(masterErr, err) log.ErrorContext(ctx, "failed to save migration log", "error", err.Error()) } } + + return masterErr } func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) error { @@ -66,7 +71,7 @@ func (s *service) MigrateSelf(ctx context.Context) error { } for _, migr := range migrations { - if !slices.ContainsFunc(migrationLogs, func(l migrationLog) bool { + if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { return l.Repository == "platforma_migrations" && l.MigrationId == migr.ID }) { err := s.ApplyMigration(ctx, migr) @@ -84,17 +89,17 @@ func (s *service) MigrateSelf(ctx context.Context) error { } func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { - _, err := s.db.ExecContext(ctx, migration.Up) + 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 { + if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { return l.Repository == migr.repository && l.MigrationId == migr.ID }) { err := s.ApplyMigration(ctx, migr) @@ -111,7 +116,7 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m } func (s *service) RevertMigration(ctx context.Context, migration Migration) error { - _, err := s.db.ExecContext(ctx, migration.Down) + err := s.repo.ExecuteQuery(ctx, migration.Down) if err != nil { return fmt.Errorf("failed to revert migration: %w", err) } diff --git a/database/service_test.go b/database/service_test.go new file mode 100644 index 0000000..27e603d --- /dev/null +++ b/database/service_test.go @@ -0,0 +1,43 @@ +package database_test + +import ( + "context" + "testing" + + "github.com/jmoiron/sqlx" + "github.com/mishankov/platforma/database" +) + +func TestSaveMigrationLogs(t *testing.T) { + t.Run("single successful", func(t *testing.T) { + repo := &repoMock{} + service := database.NewService(repo, &sqlx.DB{}) + + err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + }) +} + +type repoMock struct{} + +func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]database.MigrationLog, error) { + return nil, nil +} + +func (r *repoMock) SaveMigrationLog(ctx context.Context, log database.MigrationLog) error { + return nil +} + +func (r *repoMock) RemoveMigrationLog(ctx context.Context, repository, id string) error { + return nil +} + +func (r *repoMock) ExecuteQuery(ctx context.Context, query string) error { + return nil +} + +func (r *repoMock) Migrations() []database.Migration { + return nil +} From 2af28b3d94b3168f17654cdbf78be82cbf787f02 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:24:15 +0300 Subject: [PATCH 12/35] fix errors.Join usage --- database/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/service.go b/database/service.go index 675ce5e..5774e92 100644 --- a/database/service.go +++ b/database/service.go @@ -40,7 +40,7 @@ func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) for _, migr := range migrations { err := s.SaveMigrationLog(ctx, migr.repository, migr.ID) if err != nil { - errors.Join(masterErr, err) + masterErr = errors.Join(masterErr, err) log.ErrorContext(ctx, "failed to save migration log", "error", err.Error()) } } From 01d3f59117eb82ff7597a30c36d6a2de6f117e66 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:33:12 +0300 Subject: [PATCH 13/35] error test for SaveMigrationLogs --- database/service_test.go | 60 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/database/service_test.go b/database/service_test.go index 27e603d..3c2ad40 100644 --- a/database/service_test.go +++ b/database/service_test.go @@ -2,6 +2,7 @@ package database_test import ( "context" + "errors" "testing" "github.com/jmoiron/sqlx" @@ -9,7 +10,9 @@ import ( ) func TestSaveMigrationLogs(t *testing.T) { + t.Parallel() t.Run("single successful", func(t *testing.T) { + t.Parallel() repo := &repoMock{} service := database.NewService(repo, &sqlx.DB{}) @@ -18,15 +21,70 @@ func TestSaveMigrationLogs(t *testing.T) { t.Fatalf("expected no errors, got: %s", err.Error()) } }) + + t.Run("single error", func(t *testing.T) { + t.Parallel() + ErrSome := errors.New("some error") + repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml database.MigrationLog) error { + return ErrSome + }} + service := database.NewService(repo, &sqlx.DB{}) + + err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) + if err == nil { + t.Fatalf("expected error, got nothing") + } + + if !errors.Is(err, ErrSome) { + t.Fatalf("expected ErrSome, got: %s", err.Error()) + } + }) + + t.Run("multiple errors", func(t *testing.T) { + t.Parallel() + ErrSome := errors.New("some error") + ErrOther := errors.New("other error") + repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml database.MigrationLog) error { + if ml.MigrationId == "some id" { + return ErrSome + } + + if ml.MigrationId == "other id" { + return ErrOther + } + + return nil + }} + service := database.NewService(repo, &sqlx.DB{}) + + err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}, {ID: "other id"}}) + if err == nil { + t.Fatalf("expected error, got nothing") + } + + if !errors.Is(err, ErrSome) { + t.Fatalf("expected ErrSome, got: %s", err.Error()) + } + + if !errors.Is(err, ErrOther) { + t.Fatalf("expected ErrOther, got: %s", err.Error()) + } + }) } -type repoMock struct{} +type repoMock struct { + saveMigrationLog func(context.Context, database.MigrationLog) error +} func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]database.MigrationLog, error) { return nil, nil } func (r *repoMock) SaveMigrationLog(ctx context.Context, log database.MigrationLog) error { + if r.saveMigrationLog != nil { + return r.saveMigrationLog(ctx, log) + } + return nil } From 5fa1ea21b074d79ebc0ffb0b7eece203f2220f32 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:39:44 +0300 Subject: [PATCH 14/35] fix linter errors --- database/repository.go | 5 ++++- database/service.go | 29 +++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/database/repository.go b/database/repository.go index 3fcf05d..79e91ed 100644 --- a/database/repository.go +++ b/database/repository.go @@ -56,5 +56,8 @@ func (r *Repository) RemoveMigrationLog(ctx context.Context, repository, id stri func (r *Repository) ExecuteQuery(ctx context.Context, query string) error { _, err := r.db.ExecContext(ctx, query) - return err + if err != nil { + return fmt.Errorf("failed to execute query: %w", err) + } + return nil } diff --git a/database/service.go b/database/service.go index 5774e92..95b6253 100644 --- a/database/service.go +++ b/database/service.go @@ -28,11 +28,19 @@ func NewService(repo repository, db *sqlx.DB) *service { } func (s *service) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { - return s.repo.GetMigrationLogs(ctx) + 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 { - return s.repo.SaveMigrationLog(ctx, MigrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) + 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 { @@ -41,7 +49,6 @@ func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) err := s.SaveMigrationLog(ctx, migr.repository, migr.ID) if err != nil { masterErr = errors.Join(masterErr, err) - log.ErrorContext(ctx, "failed to save migration log", "error", err.Error()) } } @@ -49,7 +56,11 @@ func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) } func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) error { - return s.repo.RemoveMigrationLog(ctx, repository, id) + err := s.repo.RemoveMigrationLog(ctx, repository, id) + if err != nil { + return fmt.Errorf("failed to remove migration log: %w", err) + } + return nil } func (s *service) MigrateSelf(ctx context.Context) error { @@ -83,7 +94,10 @@ func (s *service) MigrateSelf(ctx context.Context) error { } } - 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()) + } return nil } @@ -110,7 +124,10 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m } } - 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()) + } return nil } From 72fa1b6528e23fd4528ec8cf5d054dbb1c015758 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:50:50 +0300 Subject: [PATCH 15/35] tests for GetMigrationLogs --- database/service_test.go | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/database/service_test.go b/database/service_test.go index 3c2ad40..a859dfb 100644 --- a/database/service_test.go +++ b/database/service_test.go @@ -72,11 +72,68 @@ func TestSaveMigrationLogs(t *testing.T) { }) } +func TestGetMigrationLogs(t *testing.T) { + t.Parallel() + t.Run("successful no logs", func(t *testing.T) { + t.Parallel() + repo := &repoMock{} + service := database.NewService(repo, &sqlx.DB{}) + + logs, err := service.GetMigrationLogs(context.TODO()) + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(logs) > 0 { + t.Fatalf("expected no logs, got: %d", len(logs)) + } + }) + + t.Run("successful some logs", func(t *testing.T) { + t.Parallel() + repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { + return []database.MigrationLog{{}, {}}, nil + }} + service := database.NewService(repo, &sqlx.DB{}) + + logs, err := service.GetMigrationLogs(context.TODO()) + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(logs) != 2 { + t.Fatalf("expected 2 logs, got: %d", len(logs)) + } + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + ErrSome := errors.New("some error") + repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { + return nil, ErrSome + }} + service := database.NewService(repo, &sqlx.DB{}) + + _, err := service.GetMigrationLogs(context.TODO()) + if err == nil { + t.Fatalf("expected error, got nothing") + } + + if !errors.Is(err, ErrSome) { + t.Fatalf("expected ErrSome, got: %s", err.Error()) + } + }) +} + type repoMock struct { + getMigrationLogs func(context.Context) ([]database.MigrationLog, error) saveMigrationLog func(context.Context, database.MigrationLog) error } func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]database.MigrationLog, error) { + if r.getMigrationLogs != nil { + return r.getMigrationLogs(ctx) + } return nil, nil } From 6849b2e5717f1f511fb1f12ea4f64a91ad5d7e75 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 20:52:24 +0300 Subject: [PATCH 16/35] delete RemoveMigrationLog method --- database/service.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/database/service.go b/database/service.go index 95b6253..2afe238 100644 --- a/database/service.go +++ b/database/service.go @@ -55,14 +55,6 @@ func (s *service) SaveMigrationLogs(ctx context.Context, migrations []Migration) return masterErr } -func (s *service) RemoveMigrationLog(ctx context.Context, repository, id string) error { - err := s.repo.RemoveMigrationLog(ctx, repository, id) - if err != nil { - return fmt.Errorf("failed to remove migration log: %w", err) - } - return nil -} - func (s *service) MigrateSelf(ctx context.Context) error { migrations := s.repo.Migrations() appliedMigrations := []Migration{} From 85d84b6ad5d1d9a9bf3d61a3f547a8cb5a122dc5 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 21:34:41 +0300 Subject: [PATCH 17/35] tests for RevertMigrations --- database/service.go | 22 +++++++++--- database/service_test.go | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/database/service.go b/database/service.go index 2afe238..eb20d75 100644 --- a/database/service.go +++ b/database/service.go @@ -66,7 +66,10 @@ func (s *service) MigrateSelf(ctx context.Context) error { for _, migr := range migrations { err := s.ApplyMigration(ctx, migr) if err != nil { - s.RevertMigrations(ctx, appliedMigrations) + revertErr := s.RevertMigrations(ctx, appliedMigrations) + if revertErr != nil { + log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + } return err } appliedMigrations = append(appliedMigrations, migr) @@ -79,7 +82,10 @@ func (s *service) MigrateSelf(ctx context.Context) error { }) { err := s.ApplyMigration(ctx, migr) if err != nil { - s.RevertMigrations(ctx, appliedMigrations) + revertErr := s.RevertMigrations(ctx, appliedMigrations) + if revertErr != nil { + log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + } return err } appliedMigrations = append(appliedMigrations, migr) @@ -110,7 +116,10 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m }) { err := s.ApplyMigration(ctx, migr) if err != nil { - s.RevertMigrations(ctx, appliedMigrations) + revertErr := s.RevertMigrations(ctx, appliedMigrations) + if revertErr != nil { + log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + } return err } } @@ -132,11 +141,14 @@ func (s *service) RevertMigration(ctx context.Context, migration Migration) erro return nil } -func (s *service) RevertMigrations(ctx context.Context, migrations []Migration) { +func (s *service) RevertMigrations(ctx context.Context, migrations []Migration) error { + masterErr := error(nil) for _, migr := range slices.Backward(migrations) { err := s.RevertMigration(ctx, migr) if err != nil { - log.ErrorContext(ctx, "failed to revert migration", "migration", migr.ID, "error", err.Error()) + masterErr = errors.Join(masterErr, fmt.Errorf("failed to revert migration %s: %w", migr.ID, err)) } } + + return masterErr } diff --git a/database/service_test.go b/database/service_test.go index a859dfb..c890c86 100644 --- a/database/service_test.go +++ b/database/service_test.go @@ -125,9 +125,79 @@ func TestGetMigrationLogs(t *testing.T) { }) } +func TestRevertMigrations(t *testing.T) { + t.Parallel() + t.Run("successful revert", func(t *testing.T) { + t.Parallel() + repo := &repoMock{} + service := database.NewService(repo, &sqlx.DB{}) + + migrations := []database.Migration{{ID: "migration1"}, {ID: "migration2"}} + + err := service.RevertMigrations(context.TODO(), migrations) + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + }) + + t.Run("revert with error", func(t *testing.T) { + t.Parallel() + ErrSome := errors.New("some error") + repo := &repoMock{ + executeQuery: func(ctx context.Context, query string) error { + return ErrSome + }, + } + service := database.NewService(repo, &sqlx.DB{}) + + err := service.RevertMigrations(context.TODO(), []database.Migration{{ID: "migration1"}}) + if err == nil { + t.Fatalf("expected error, got nothing") + } + + if !errors.Is(err, ErrSome) { + t.Fatalf("expected ErrSome, got: %s", err.Error()) + } + }) + + t.Run("revert with multiple errors", func(t *testing.T) { + t.Parallel() + ErrSome := errors.New("some error") + ErrOther := errors.New("other error") + repo := &repoMock{ + executeQuery: func(ctx context.Context, query string) error { + if query == "migration1" { + return ErrSome + } + + if query == "migration2" { + return ErrOther + } + + return nil + }, + } + service := database.NewService(repo, &sqlx.DB{}) + + err := service.RevertMigrations(context.TODO(), []database.Migration{{Down: "migration1"}, {Down: "migration2"}}) + if err == nil { + t.Fatalf("expected error, got nothing") + } + + if !errors.Is(err, ErrSome) { + t.Fatalf("expected ErrSome, got: %s", err.Error()) + } + + if !errors.Is(err, ErrOther) { + t.Fatalf("expected ErrOther, got: %s", err.Error()) + } + }) +} + type repoMock struct { getMigrationLogs func(context.Context) ([]database.MigrationLog, error) saveMigrationLog func(context.Context, database.MigrationLog) error + executeQuery func(context.Context, string) error } func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]database.MigrationLog, error) { @@ -150,6 +220,9 @@ func (r *repoMock) RemoveMigrationLog(ctx context.Context, repository, id string } func (r *repoMock) ExecuteQuery(ctx context.Context, query string) error { + if r.executeQuery != nil { + return r.executeQuery(ctx, query) + } return nil } From b6c09ae4edd3f013d688f7a3cd37cc48c0760ac1 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 21:36:33 +0300 Subject: [PATCH 18/35] remove db dependency from service --- database/database.go | 2 +- database/service.go | 3 +-- database/service_test.go | 19 +++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/database/database.go b/database/database.go index c0c97f0..9479d63 100644 --- a/database/database.go +++ b/database/database.go @@ -23,7 +23,7 @@ func New(connection string) (*Database, error) { } repository := newRepository(db) - service := NewService(repository, db) + service := NewService(repository) return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil } diff --git a/database/service.go b/database/service.go index eb20d75..babc98b 100644 --- a/database/service.go +++ b/database/service.go @@ -7,7 +7,6 @@ import ( "slices" "time" - "github.com/jmoiron/sqlx" "github.com/mishankov/platforma/log" ) @@ -23,7 +22,7 @@ type service struct { repo repository } -func NewService(repo repository, db *sqlx.DB) *service { +func NewService(repo repository) *service { return &service{repo: repo} } diff --git a/database/service_test.go b/database/service_test.go index c890c86..32a408b 100644 --- a/database/service_test.go +++ b/database/service_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/jmoiron/sqlx" "github.com/mishankov/platforma/database" ) @@ -14,7 +13,7 @@ func TestSaveMigrationLogs(t *testing.T) { t.Run("single successful", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) if err != nil { @@ -28,7 +27,7 @@ func TestSaveMigrationLogs(t *testing.T) { repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml database.MigrationLog) error { return ErrSome }} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) if err == nil { @@ -55,7 +54,7 @@ func TestSaveMigrationLogs(t *testing.T) { return nil }} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}, {ID: "other id"}}) if err == nil { @@ -77,7 +76,7 @@ func TestGetMigrationLogs(t *testing.T) { t.Run("successful no logs", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) logs, err := service.GetMigrationLogs(context.TODO()) if err != nil { @@ -94,7 +93,7 @@ func TestGetMigrationLogs(t *testing.T) { repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { return []database.MigrationLog{{}, {}}, nil }} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) logs, err := service.GetMigrationLogs(context.TODO()) if err != nil { @@ -112,7 +111,7 @@ func TestGetMigrationLogs(t *testing.T) { repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { return nil, ErrSome }} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) _, err := service.GetMigrationLogs(context.TODO()) if err == nil { @@ -130,7 +129,7 @@ func TestRevertMigrations(t *testing.T) { t.Run("successful revert", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) migrations := []database.Migration{{ID: "migration1"}, {ID: "migration2"}} @@ -148,7 +147,7 @@ func TestRevertMigrations(t *testing.T) { return ErrSome }, } - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) err := service.RevertMigrations(context.TODO(), []database.Migration{{ID: "migration1"}}) if err == nil { @@ -177,7 +176,7 @@ func TestRevertMigrations(t *testing.T) { return nil }, } - service := database.NewService(repo, &sqlx.DB{}) + service := database.NewService(repo) err := service.RevertMigrations(context.TODO(), []database.Migration{{Down: "migration1"}, {Down: "migration2"}}) if err == nil { From 63acaddf7949b3951f296af1ea5d16ccc9b8296f Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Mon, 27 Oct 2025 21:37:48 +0300 Subject: [PATCH 19/35] add check task --- Taskfile.dist.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml index d9a0a01..fe1f1e8 100644 --- a/Taskfile.dist.yml +++ b/Taskfile.dist.yml @@ -1,4 +1,4 @@ -version: '3' +version: "3" tasks: lint: @@ -12,3 +12,8 @@ tasks: test: cmds: - go test -cover ./... + + check: + deps: + - lint + - test From f8f8b12a56c12c9a1cc868f4e02f4996dd9f8199 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 11:05:36 +0300 Subject: [PATCH 20/35] Make service constructor internal --- database/database.go | 2 +- database/service.go | 2 +- database/service_test.go | 54 +++++++++++++++++++--------------------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/database/database.go b/database/database.go index 9479d63..55c1dcd 100644 --- a/database/database.go +++ b/database/database.go @@ -23,7 +23,7 @@ func New(connection string) (*Database, error) { } repository := newRepository(db) - service := NewService(repository) + service := newService(repository) return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil } diff --git a/database/service.go b/database/service.go index babc98b..e2bec36 100644 --- a/database/service.go +++ b/database/service.go @@ -22,7 +22,7 @@ type service struct { repo repository } -func NewService(repo repository) *service { +func newService(repo repository) *service { return &service{repo: repo} } diff --git a/database/service_test.go b/database/service_test.go index 32a408b..1a4794f 100644 --- a/database/service_test.go +++ b/database/service_test.go @@ -1,11 +1,9 @@ -package database_test +package database import ( "context" "errors" "testing" - - "github.com/mishankov/platforma/database" ) func TestSaveMigrationLogs(t *testing.T) { @@ -13,9 +11,9 @@ func TestSaveMigrationLogs(t *testing.T) { t.Run("single successful", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo) + service := newService(repo) - err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) + err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}}) if err != nil { t.Fatalf("expected no errors, got: %s", err.Error()) } @@ -24,12 +22,12 @@ func TestSaveMigrationLogs(t *testing.T) { t.Run("single error", func(t *testing.T) { t.Parallel() ErrSome := errors.New("some error") - repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml database.MigrationLog) error { + repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml MigrationLog) error { return ErrSome }} - service := database.NewService(repo) + service := newService(repo) - err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}}) + err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}}) if err == nil { t.Fatalf("expected error, got nothing") } @@ -43,7 +41,7 @@ func TestSaveMigrationLogs(t *testing.T) { t.Parallel() ErrSome := errors.New("some error") ErrOther := errors.New("other error") - repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml database.MigrationLog) error { + repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml MigrationLog) error { if ml.MigrationId == "some id" { return ErrSome } @@ -54,9 +52,9 @@ func TestSaveMigrationLogs(t *testing.T) { return nil }} - service := database.NewService(repo) + service := newService(repo) - err := service.SaveMigrationLogs(context.TODO(), []database.Migration{{ID: "some id"}, {ID: "other id"}}) + err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}, {ID: "other id"}}) if err == nil { t.Fatalf("expected error, got nothing") } @@ -76,7 +74,7 @@ func TestGetMigrationLogs(t *testing.T) { t.Run("successful no logs", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo) + service := newService(repo) logs, err := service.GetMigrationLogs(context.TODO()) if err != nil { @@ -90,10 +88,10 @@ func TestGetMigrationLogs(t *testing.T) { t.Run("successful some logs", func(t *testing.T) { t.Parallel() - repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { - return []database.MigrationLog{{}, {}}, nil + repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]MigrationLog, error) { + return []MigrationLog{{}, {}}, nil }} - service := database.NewService(repo) + service := newService(repo) logs, err := service.GetMigrationLogs(context.TODO()) if err != nil { @@ -108,10 +106,10 @@ func TestGetMigrationLogs(t *testing.T) { t.Run("error", func(t *testing.T) { t.Parallel() ErrSome := errors.New("some error") - repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]database.MigrationLog, error) { + repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]MigrationLog, error) { return nil, ErrSome }} - service := database.NewService(repo) + service := newService(repo) _, err := service.GetMigrationLogs(context.TODO()) if err == nil { @@ -129,9 +127,9 @@ func TestRevertMigrations(t *testing.T) { t.Run("successful revert", func(t *testing.T) { t.Parallel() repo := &repoMock{} - service := database.NewService(repo) + service := newService(repo) - migrations := []database.Migration{{ID: "migration1"}, {ID: "migration2"}} + migrations := []Migration{{ID: "migration1"}, {ID: "migration2"}} err := service.RevertMigrations(context.TODO(), migrations) if err != nil { @@ -147,9 +145,9 @@ func TestRevertMigrations(t *testing.T) { return ErrSome }, } - service := database.NewService(repo) + service := newService(repo) - err := service.RevertMigrations(context.TODO(), []database.Migration{{ID: "migration1"}}) + err := service.RevertMigrations(context.TODO(), []Migration{{ID: "migration1"}}) if err == nil { t.Fatalf("expected error, got nothing") } @@ -176,9 +174,9 @@ func TestRevertMigrations(t *testing.T) { return nil }, } - service := database.NewService(repo) + service := newService(repo) - err := service.RevertMigrations(context.TODO(), []database.Migration{{Down: "migration1"}, {Down: "migration2"}}) + err := service.RevertMigrations(context.TODO(), []Migration{{Down: "migration1"}, {Down: "migration2"}}) if err == nil { t.Fatalf("expected error, got nothing") } @@ -194,19 +192,19 @@ func TestRevertMigrations(t *testing.T) { } type repoMock struct { - getMigrationLogs func(context.Context) ([]database.MigrationLog, error) - saveMigrationLog func(context.Context, database.MigrationLog) error + getMigrationLogs func(context.Context) ([]MigrationLog, error) + saveMigrationLog func(context.Context, MigrationLog) error executeQuery func(context.Context, string) error } -func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]database.MigrationLog, error) { +func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { if r.getMigrationLogs != nil { return r.getMigrationLogs(ctx) } return nil, nil } -func (r *repoMock) SaveMigrationLog(ctx context.Context, log database.MigrationLog) error { +func (r *repoMock) SaveMigrationLog(ctx context.Context, log MigrationLog) error { if r.saveMigrationLog != nil { return r.saveMigrationLog(ctx, log) } @@ -225,6 +223,6 @@ func (r *repoMock) ExecuteQuery(ctx context.Context, query string) error { return nil } -func (r *repoMock) Migrations() []database.Migration { +func (r *repoMock) Migrations() []Migration { return nil } From 64a476732e25004f0583f999c98b751a580d1805 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 11:36:24 +0300 Subject: [PATCH 21/35] Add database integration tests with testcontainers --- .golangci.yml | 13 ++- Taskfile.dist.yml | 2 +- database/database_test.go | 96 ++++++++++++++++ database/service_test.go | 228 -------------------------------------- go.mod | 59 ++++++++++ go.sum | 202 +++++++++++++++++++++++++++++++++ 6 files changed, 367 insertions(+), 233 deletions(-) create mode 100644 database/database_test.go delete mode 100644 database/service_test.go diff --git a/.golangci.yml b/.golangci.yml index 1f7a1ba..7291788 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -65,10 +65,15 @@ linters: rules: - path: _test\.go linters: - - err113 - - errcheck - - gosec - - nilnil + - err113 + - errcheck + - gosec + - nilnil + + - path: database_test\.go + linters: + - paralleltest + - tparallel formatters: enable: diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml index fe1f1e8..a2d84ba 100644 --- a/Taskfile.dist.yml +++ b/Taskfile.dist.yml @@ -11,7 +11,7 @@ tasks: test: cmds: - - go test -cover ./... + - go test -cover -v ./... check: deps: diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..f4dd123 --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,96 @@ +package database_test + +import ( + "context" + "testing" + + "github.com/mishankov/platforma/database" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func TestMigrate(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ctr, err := postgres.Run( + ctx, + "postgres:18-alpine", + postgres.WithDatabase("hostamat"), + postgres.WithUsername("hostamat"), + postgres.WithPassword("hostamat"), + postgres.BasicWaitStrategies(), + ) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + err = ctr.Snapshot(ctx) + if err != nil { + t.Fatalf("failed to create snapshot: %s", err.Error()) + } + + dbURL, err := ctr.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatalf("failed to get connection string: %s", err.Error()) + } + + t.Logf("db connection string: %s", dbURL) + + t.Run("initialize and migrate empty database", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + if err != nil { + t.Fatalf("failed to restore db: %s", err.Error()) + } + }) + + db, err := database.New(dbURL) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + if db == nil { + t.Fatalf("database is nil") + } + + err = db.Migrate(ctx) + if err != nil { + t.Fatalf("failed to migrate database: %s", err.Error()) + } + }) + + t.Run("migrate database with single repository", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + if err != nil { + t.Fatalf("failed to restore db: %s", err.Error()) + } + }) + + db, err := database.New(dbURL) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + if db == nil { + t.Fatalf("database is nil") + } + + db.RegisterRepository("some_repo", simpleRepo{}) + + err = db.Migrate(ctx) + if err != nil { + t.Fatalf("failed to migrate database: %s", err.Error()) + } + }) +} + +type simpleRepo struct{} + +func (r simpleRepo) Migrations() []database.Migration { + return []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", + Down: "DROP TABLE simple_repo", + }} +} diff --git a/database/service_test.go b/database/service_test.go deleted file mode 100644 index 1a4794f..0000000 --- a/database/service_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package database - -import ( - "context" - "errors" - "testing" -) - -func TestSaveMigrationLogs(t *testing.T) { - t.Parallel() - t.Run("single successful", func(t *testing.T) { - t.Parallel() - repo := &repoMock{} - service := newService(repo) - - err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}}) - if err != nil { - t.Fatalf("expected no errors, got: %s", err.Error()) - } - }) - - t.Run("single error", func(t *testing.T) { - t.Parallel() - ErrSome := errors.New("some error") - repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml MigrationLog) error { - return ErrSome - }} - service := newService(repo) - - err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}}) - if err == nil { - t.Fatalf("expected error, got nothing") - } - - if !errors.Is(err, ErrSome) { - t.Fatalf("expected ErrSome, got: %s", err.Error()) - } - }) - - t.Run("multiple errors", func(t *testing.T) { - t.Parallel() - ErrSome := errors.New("some error") - ErrOther := errors.New("other error") - repo := &repoMock{saveMigrationLog: func(ctx context.Context, ml MigrationLog) error { - if ml.MigrationId == "some id" { - return ErrSome - } - - if ml.MigrationId == "other id" { - return ErrOther - } - - return nil - }} - service := newService(repo) - - err := service.SaveMigrationLogs(context.TODO(), []Migration{{ID: "some id"}, {ID: "other id"}}) - if err == nil { - t.Fatalf("expected error, got nothing") - } - - if !errors.Is(err, ErrSome) { - t.Fatalf("expected ErrSome, got: %s", err.Error()) - } - - if !errors.Is(err, ErrOther) { - t.Fatalf("expected ErrOther, got: %s", err.Error()) - } - }) -} - -func TestGetMigrationLogs(t *testing.T) { - t.Parallel() - t.Run("successful no logs", func(t *testing.T) { - t.Parallel() - repo := &repoMock{} - service := newService(repo) - - logs, err := service.GetMigrationLogs(context.TODO()) - if err != nil { - t.Fatalf("expected no errors, got: %s", err.Error()) - } - - if len(logs) > 0 { - t.Fatalf("expected no logs, got: %d", len(logs)) - } - }) - - t.Run("successful some logs", func(t *testing.T) { - t.Parallel() - repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]MigrationLog, error) { - return []MigrationLog{{}, {}}, nil - }} - service := newService(repo) - - logs, err := service.GetMigrationLogs(context.TODO()) - if err != nil { - t.Fatalf("expected no errors, got: %s", err.Error()) - } - - if len(logs) != 2 { - t.Fatalf("expected 2 logs, got: %d", len(logs)) - } - }) - - t.Run("error", func(t *testing.T) { - t.Parallel() - ErrSome := errors.New("some error") - repo := &repoMock{getMigrationLogs: func(ctx context.Context) ([]MigrationLog, error) { - return nil, ErrSome - }} - service := newService(repo) - - _, err := service.GetMigrationLogs(context.TODO()) - if err == nil { - t.Fatalf("expected error, got nothing") - } - - if !errors.Is(err, ErrSome) { - t.Fatalf("expected ErrSome, got: %s", err.Error()) - } - }) -} - -func TestRevertMigrations(t *testing.T) { - t.Parallel() - t.Run("successful revert", func(t *testing.T) { - t.Parallel() - repo := &repoMock{} - service := newService(repo) - - migrations := []Migration{{ID: "migration1"}, {ID: "migration2"}} - - err := service.RevertMigrations(context.TODO(), migrations) - if err != nil { - t.Fatalf("expected no errors, got: %s", err.Error()) - } - }) - - t.Run("revert with error", func(t *testing.T) { - t.Parallel() - ErrSome := errors.New("some error") - repo := &repoMock{ - executeQuery: func(ctx context.Context, query string) error { - return ErrSome - }, - } - service := newService(repo) - - err := service.RevertMigrations(context.TODO(), []Migration{{ID: "migration1"}}) - if err == nil { - t.Fatalf("expected error, got nothing") - } - - if !errors.Is(err, ErrSome) { - t.Fatalf("expected ErrSome, got: %s", err.Error()) - } - }) - - t.Run("revert with multiple errors", func(t *testing.T) { - t.Parallel() - ErrSome := errors.New("some error") - ErrOther := errors.New("other error") - repo := &repoMock{ - executeQuery: func(ctx context.Context, query string) error { - if query == "migration1" { - return ErrSome - } - - if query == "migration2" { - return ErrOther - } - - return nil - }, - } - service := newService(repo) - - err := service.RevertMigrations(context.TODO(), []Migration{{Down: "migration1"}, {Down: "migration2"}}) - if err == nil { - t.Fatalf("expected error, got nothing") - } - - if !errors.Is(err, ErrSome) { - t.Fatalf("expected ErrSome, got: %s", err.Error()) - } - - if !errors.Is(err, ErrOther) { - t.Fatalf("expected ErrOther, got: %s", err.Error()) - } - }) -} - -type repoMock struct { - getMigrationLogs func(context.Context) ([]MigrationLog, error) - saveMigrationLog func(context.Context, MigrationLog) error - executeQuery func(context.Context, string) error -} - -func (r *repoMock) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { - if r.getMigrationLogs != nil { - return r.getMigrationLogs(ctx) - } - return nil, nil -} - -func (r *repoMock) SaveMigrationLog(ctx context.Context, log MigrationLog) error { - if r.saveMigrationLog != nil { - return r.saveMigrationLog(ctx, log) - } - - return nil -} - -func (r *repoMock) RemoveMigrationLog(ctx context.Context, repository, id string) error { - return nil -} - -func (r *repoMock) ExecuteQuery(ctx context.Context, query string) error { - if r.executeQuery != nil { - return r.executeQuery(ctx, query) - } - return nil -} - -func (r *repoMock) Migrations() []Migration { - return nil -} diff --git a/go.mod b/go.mod index 15446ed..5766122 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,64 @@ require ( github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 + github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 golang.org/x/crypto v0.43.0 ) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/testcontainers/testcontainers-go v0.39.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.37.0 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index eea830b..6e7928a 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,216 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= +github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= +github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM= +github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0/go.mod h1:4K2OhtHEeT+JSIFX4V8DkGKsyLa96Y2vLdd3xsxD5HE= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= +google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= From ff476366c851260690506720aae28d34ff4f68ae Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 11:46:16 +0300 Subject: [PATCH 22/35] Add macOS Docker socket override for Testcontainers Set TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE environment variable to fix Docker connectivity in macOS CI runners. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3481c6..ffb9ae0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,10 @@ jobs: with: go-version: stable + - name: macos docker fix + if: ${{ matrix.os == 'macos' }} + run: export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock + - name: Test run: go test -v -coverprofile="coverage.txt" ./... From 5777ed7ae74b927679a6f04db7937b6fd9493f92 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 11:51:03 +0300 Subject: [PATCH 23/35] Remove macOS from CI matrix --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffb9ae0..96c017e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ matrix.os }}-latest strategy: matrix: - os: [ubuntu, windows, macos] + os: [ubuntu, windows] steps: - uses: actions/checkout@v5 @@ -38,10 +38,6 @@ jobs: with: go-version: stable - - name: macos docker fix - if: ${{ matrix.os == 'macos' }} - run: export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock - - name: Test run: go test -v -coverprofile="coverage.txt" ./... From 7a386799740c5b7014c21df3c66cefb8c43e6542 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 11:59:22 +0300 Subject: [PATCH 24/35] Remove Windows from CI matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96c017e..9924ee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ matrix.os }}-latest strategy: matrix: - os: [ubuntu, windows] + os: [ubuntu] steps: - uses: actions/checkout@v5 From 1943b798c94a25b84618e28d86f5053d3d389beb Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 12:05:56 +0300 Subject: [PATCH 25/35] Remove unused RemoveMigrationLog method --- database/repository.go | 9 --------- database/service.go | 1 - 2 files changed, 10 deletions(-) diff --git a/database/repository.go b/database/repository.go index 79e91ed..7ab923d 100644 --- a/database/repository.go +++ b/database/repository.go @@ -45,15 +45,6 @@ func (r *Repository) SaveMigrationLog(ctx context.Context, log MigrationLog) err return nil } -func (r *Repository) RemoveMigrationLog(ctx context.Context, repository, id string) error { - query := `DELETE FROM platforma_migrations WHERE repository = $1 AND id = $2` - _, err := r.db.ExecContext(ctx, query, repository, id) - if err != nil { - return fmt.Errorf("failed to remove migration log: %w", err) - } - return nil -} - func (r *Repository) ExecuteQuery(ctx context.Context, query string) error { _, err := r.db.ExecContext(ctx, query) if err != nil { diff --git a/database/service.go b/database/service.go index e2bec36..83862fe 100644 --- a/database/service.go +++ b/database/service.go @@ -13,7 +13,6 @@ import ( type repository interface { GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) SaveMigrationLog(ctx context.Context, log MigrationLog) error - RemoveMigrationLog(ctx context.Context, repository, id string) error ExecuteQuery(ctx context.Context, query string) error Migrations() []Migration } From 8ce6475526abe04d0bfaae8095d96d9d26052792 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 12:13:42 +0300 Subject: [PATCH 26/35] Add database migration test cases - Test migration with multiple repositories - Test migration failure scenario - Refactor simpleRepo to support custom migrations --- database/database_test.go | 91 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/database/database_test.go b/database/database_test.go index f4dd123..a03d0cd 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -76,21 +76,98 @@ func TestMigrate(t *testing.T) { t.Fatalf("database is nil") } - db.RegisterRepository("some_repo", simpleRepo{}) + db.RegisterRepository("some_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", + Down: "DROP TABLE simple_repo", + }}}) err = db.Migrate(ctx) if err != nil { t.Fatalf("failed to migrate database: %s", err.Error()) } }) + + t.Run("migrate database with multiple repositories", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + if err != nil { + t.Fatalf("failed to restore db: %s", err.Error()) + } + }) + + db, err := database.New(dbURL) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + if db == nil { + t.Fatalf("database is nil") + } + + db.RegisterRepository("some_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", + Down: "DROP TABLE simple_repo", + }}}) + + db.RegisterRepository("other_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS other_repo (id TEXT)", + Down: "DROP TABLE other_repo", + }}}) + + err = db.Migrate(ctx) + if err != nil { + t.Fatalf("failed to migrate database: %s", err.Error()) + } + }) + + t.Run("migrate database with failing migration", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + if err != nil { + t.Fatalf("failed to restore db: %s", err.Error()) + } + }) + + db, err := database.New(dbURL) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + if db == nil { + t.Fatalf("database is nil") + } + + db.RegisterRepository("some_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", + Down: "DROP TABLE simple_repo", + }}}) + + db.RegisterRepository("other_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS other_repo (id TEXT)", + Down: "DROP TABLE other_repo", + }, { + ID: "failing", + Up: "not even SQL here", + Down: "no need for this", + }}}) + + err = db.Migrate(ctx) + if err == nil { + t.Fatalf("migration expected to fail") + } + t.Logf("migration error: %s", err.Error()) + }) } -type simpleRepo struct{} +type simpleRepo struct { + migrations []database.Migration +} func (r simpleRepo) Migrations() []database.Migration { - return []database.Migration{{ - ID: "init", - Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", - Down: "DROP TABLE simple_repo", - }} + return r.migrations } From e9c70197054ab174a8ad628d2ad06c85af547088 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 12:19:26 +0300 Subject: [PATCH 27/35] Track applied migrations during execution --- database/service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/database/service.go b/database/service.go index 83862fe..2686780 100644 --- a/database/service.go +++ b/database/service.go @@ -120,6 +120,7 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m } return err } + appliedMigrations = append(appliedMigrations, migr) } } From 6bc1bc22c05df3418fa5d566fc0f57e4e900dce3 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 12:38:57 +0300 Subject: [PATCH 28/35] Add test for migration failure with revert --- database/database_test.go | 40 +++++++++++++++++++++++++++++++++++++++ database/service.go | 6 +++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/database/database_test.go b/database/database_test.go index a03d0cd..0222de5 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -162,6 +162,46 @@ func TestMigrate(t *testing.T) { } t.Logf("migration error: %s", err.Error()) }) + + t.Run("migrate database with failing migration and revert", func(t *testing.T) { + t.Cleanup(func() { + err = ctr.Restore(ctx) + if err != nil { + t.Fatalf("failed to restore db: %s", err.Error()) + } + }) + + db, err := database.New(dbURL) + if err != nil { + t.Fatalf("failed to initialize database: %s", err.Error()) + } + + if db == nil { + t.Fatalf("database is nil") + } + + db.RegisterRepository("some_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS simple_repo (id TEXT)", + Down: "broken SQL", + }}}) + + db.RegisterRepository("other_repo", simpleRepo{migrations: []database.Migration{{ + ID: "init", + Up: "CREATE TABLE IF NOT EXISTS other_repo (id TEXT)", + Down: "DROP TABLE other_repo", + }, { + ID: "failing", + Up: "not even SQL here", + Down: "no need for this", + }}}) + + err = db.Migrate(ctx) + if err == nil { + t.Fatalf("migration expected to fail") + } + t.Logf("migration error: %s", err.Error()) + }) } type simpleRepo struct { diff --git a/database/service.go b/database/service.go index 2686780..d174d35 100644 --- a/database/service.go +++ b/database/service.go @@ -66,7 +66,7 @@ func (s *service) MigrateSelf(ctx context.Context) error { if err != nil { revertErr := s.RevertMigrations(ctx, appliedMigrations) if revertErr != nil { - log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) } return err } @@ -82,7 +82,7 @@ func (s *service) MigrateSelf(ctx context.Context) error { if err != nil { revertErr := s.RevertMigrations(ctx, appliedMigrations) if revertErr != nil { - log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) } return err } @@ -116,7 +116,7 @@ func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, m if err != nil { revertErr := s.RevertMigrations(ctx, appliedMigrations) if revertErr != nil { - log.ErrorContext(ctx, "got error(s) trying to revert migrations: %s", revertErr.Error()) + log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) } return err } From 8ec3230bd7d0d2ccc01352369a181b86241761a5 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 12:54:26 +0300 Subject: [PATCH 29/35] Disable dupl linter for test files --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 7291788..80952c0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -65,6 +65,7 @@ linters: rules: - path: _test\.go linters: + - dupl - err113 - errcheck - gosec From 0311e7f6269a3085f5a4ec9cd37202069da648bb Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 13:27:16 +0300 Subject: [PATCH 30/35] Add migration log verification to database tests --- database/database_test.go | 35 +++++++++++++++++++++++++++++++++++ database/service.go | 15 ++------------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/database/database_test.go b/database/database_test.go index 0222de5..366480d 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -2,6 +2,7 @@ package database_test import ( "context" + "slices" "testing" "github.com/mishankov/platforma/database" @@ -57,6 +58,24 @@ func TestMigrate(t *testing.T) { if err != nil { t.Fatalf("failed to migrate database: %s", err.Error()) } + + var migrationLogs []database.MigrationLog + err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(migrationLogs) != 1 { + t.Fatalf("expected single migration, got: %d", len(migrationLogs)) + } + + if migrationLogs[0].Repository != "platforma_migration" { + 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) + } }) t.Run("migrate database with single repository", func(t *testing.T) { @@ -86,6 +105,22 @@ func TestMigrate(t *testing.T) { if err != nil { t.Fatalf("failed to migrate database: %s", err.Error()) } + + var migrationLogs []database.MigrationLog + err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(migrationLogs) != 2 { + t.Fatalf("expected two migrations, got: %d", len(migrationLogs)) + } + + if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { + return log.Repository == "some_repo" && log.MigrationId == "init" + }) { + t.Fatalf("expected migration log to contain init migration of some_repo") + } }) t.Run("migrate database with multiple repositories", func(t *testing.T) { diff --git a/database/service.go b/database/service.go index d174d35..3d66b56 100644 --- a/database/service.go +++ b/database/service.go @@ -58,20 +58,8 @@ func (s *service) MigrateSelf(ctx context.Context) error { appliedMigrations := []Migration{} migrationLogs, err := s.repo.GetMigrationLogs(ctx) - // If GetMigrationLogs returns error, log table probably does not exist, - // so we should apply all migrations for it if err != nil { - for _, migr := range migrations { - err := s.ApplyMigration(ctx, migr) - if err != nil { - revertErr := s.RevertMigrations(ctx, appliedMigrations) - if revertErr != nil { - log.ErrorContext(ctx, "got error(s) trying to revert migrations", "error", revertErr) - } - return err - } - appliedMigrations = append(appliedMigrations, migr) - } + log.InfoContext(ctx, "migrations log table does not exist yet") } for _, migr := range migrations { @@ -86,6 +74,7 @@ func (s *service) MigrateSelf(ctx context.Context) error { } return err } + migr.repository = "platforma_migration" appliedMigrations = append(appliedMigrations, migr) } } From ce37e2ae52a8bfe9d3044138852c37a0b2b60c0d Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 13:36:10 +0300 Subject: [PATCH 31/35] Update migration test expectations The test now expects 3 total migrations instead of 2 after adding a second repository migration. Also fix error message wording. --- database/database_test.go | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/database/database_test.go b/database/database_test.go index 366480d..557cd18 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -112,14 +112,15 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected no errors, got: %s", err.Error()) } + // 2 = platforma_migrations + simple_repo if len(migrationLogs) != 2 { - t.Fatalf("expected two migrations, got: %d", len(migrationLogs)) + 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" }) { - t.Fatalf("expected migration log to contain init migration of some_repo") + t.Fatalf("expected migration log to contain init migration for some_repo") } }) @@ -156,6 +157,29 @@ func TestMigrate(t *testing.T) { if err != nil { t.Fatalf("failed to migrate database: %s", err.Error()) } + + var migrationLogs []database.MigrationLog + err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + // 3 = platforma_migrations + simple_repo + if len(migrationLogs) != 3 { + 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" + }) { + t.Fatalf("expected migration log to contain init migration for some_repo") + } + + if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { + return log.Repository == "other_repo" && log.MigrationId == "init" + }) { + t.Fatalf("expected migration log to contain init migration for other_repo") + } }) t.Run("migrate database with failing migration", func(t *testing.T) { From c06d40ac9f7fef7f776974de620611a896f40d61 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 13:41:39 +0300 Subject: [PATCH 32/35] Add table existence checks to migration tests Verify that tables are properly created after migrations by executing SELECT queries against the expected tables. --- database/database_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/database/database_test.go b/database/database_test.go index 557cd18..637ca20 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -122,6 +122,11 @@ func TestMigrate(t *testing.T) { }) { t.Fatalf("expected migration log to contain init migration for some_repo") } + + _, err = db.ExecContext(ctx, "SELECT * FROM simple_repo") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } }) t.Run("migrate database with multiple repositories", func(t *testing.T) { @@ -175,10 +180,20 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected migration log to contain init migration for some_repo") } + _, err = db.ExecContext(ctx, "SELECT * FROM simple_repo") + if err != nil { + 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" }) { - t.Fatalf("expected migration log to contain init migration for other_repo") + t.Fatalf("expected migration log to contain init migration for other_repo, but only got: %s", migrationLogs) + } + + _, err = db.ExecContext(ctx, "SELECT * FROM other_repo") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) } }) From 13180fef5b3905bd9b4de18f1dd9d48431d9e54a Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 14:48:15 +0300 Subject: [PATCH 33/35] Update migration test expectations and add rollback verification --- database/database_test.go | 44 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/database/database_test.go b/database/database_test.go index 637ca20..884bb0b 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -169,7 +169,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected no errors, got: %s", err.Error()) } - // 3 = platforma_migrations + simple_repo + // 3 = platforma_migrations + repos2 if len(migrationLogs) != 3 { t.Fatalf("expected 3 migrations, got: %d", len(migrationLogs)) } @@ -235,6 +235,48 @@ func TestMigrate(t *testing.T) { t.Fatalf("migration expected to fail") } t.Logf("migration error: %s", err.Error()) + + var migrationLogs []database.MigrationLog + err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(migrationLogs) != 1 { + t.Fatalf("expected 1 migration, got: %d", len(migrationLogs)) + } + + if migrationLogs[0].Repository != "platforma_migration" { + 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) + } + + // because migration should be reverted + if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { + return log.Repository == "some_repo" && log.MigrationId == "init" + }) { + t.Fatalf("expected migration log to not contain init migration for some_repo") + } + + _, err = db.ExecContext(ctx, "SELECT * FROM simple_repo") + if err == nil { + t.Fatalf("expected error, got nill") + } + + // because migration should be reverted + if slices.ContainsFunc(migrationLogs, func(log database.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) + } + + _, err = db.ExecContext(ctx, "SELECT * FROM other_repo") + if err == nil { + t.Fatalf("expected error, got nill") + } }) t.Run("migrate database with failing migration and revert", func(t *testing.T) { From 4513942bc0e67de2679b5437709f317923fe7807 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 14:51:31 +0300 Subject: [PATCH 34/35] Update migration test expectations and add rollback verification --- database/database_test.go | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/database/database_test.go b/database/database_test.go index 884bb0b..731b54f 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -169,7 +169,7 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected no errors, got: %s", err.Error()) } - // 3 = platforma_migrations + repos2 + // 3 = platforma_migrations + repos if len(migrationLogs) != 3 { t.Fatalf("expected 3 migrations, got: %d", len(migrationLogs)) } @@ -317,6 +317,49 @@ func TestMigrate(t *testing.T) { t.Fatalf("migration expected to fail") } t.Logf("migration error: %s", err.Error()) + + var migrationLogs []database.MigrationLog + err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + if len(migrationLogs) != 1 { + t.Fatalf("expected 1 migration, got: %d", len(migrationLogs)) + } + + if migrationLogs[0].Repository != "platforma_migration" { + 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) + } + + // because migration should be reverted... + if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { + return log.Repository == "some_repo" && log.MigrationId == "init" + }) { + t.Fatalf("expected migration log to not contain init migration for some_repo") + } + + // ... but it also should fail to revert, so table exists + _, err = db.ExecContext(ctx, "SELECT * FROM simple_repo") + if err != nil { + t.Fatalf("expected no errors, got: %s", err.Error()) + } + + // because migration should be reverted + if slices.ContainsFunc(migrationLogs, func(log database.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) + } + + _, err = db.ExecContext(ctx, "SELECT * FROM other_repo") + if err == nil { + t.Fatalf("expected error, got nill") + } }) } From 21f07d39f5a701f81c29a8a6a806be02efd47dc9 Mon Sep 17 00:00:00 2001 From: Denis Mishankov Date: Tue, 28 Oct 2025 14:57:23 +0300 Subject: [PATCH 35/35] Remove goconst linter from test and update migration test Remove the table existence check from the test since we're now expecting the migration to be fully reverted or not attempted. --- .golangci.yml | 1 + database/database_test.go | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 80952c0..deb9368 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -68,6 +68,7 @@ linters: - dupl - err113 - errcheck + - goconst - gosec - nilnil diff --git a/database/database_test.go b/database/database_test.go index 731b54f..2e4352f 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -336,19 +336,13 @@ func TestMigrate(t *testing.T) { t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId) } - // because migration should be reverted... + // 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" }) { t.Fatalf("expected migration log to not contain init migration for some_repo") } - // ... but it also should fail to revert, so table exists - _, err = db.ExecContext(ctx, "SELECT * FROM simple_repo") - if err != nil { - t.Fatalf("expected no errors, got: %s", err.Error()) - } - // because migration should be reverted if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool { return log.Repository == "other_repo" && log.MigrationId == "init"