Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9872210
fix(database): correct interface name from shemer to schemer
mishankov Oct 26, 2025
0a64e50
feat(database): add repository layer for migration management
mishankov Oct 26, 2025
98b5ef1
feat(database): add service layer for migration management
mishankov Oct 26, 2025
3ed4436
refactor(database): extract migration logic to service layer
mishankov Oct 26, 2025
6ede75d
feat(tooling): add taskfile for golangci-lint integration
mishankov Oct 26, 2025
a33618f
feat(database): improve error handling in migration operations
mishankov Oct 26, 2025
ec81239
feat(tooling): add test task to taskfile
mishankov Oct 26, 2025
811e6e1
refactor(database): migrations without schemas
mishankov Oct 27, 2025
60dd5c8
create and use SaveMigrationLogs
mishankov Oct 27, 2025
d5e3f91
decouple service and repository
mishankov Oct 27, 2025
0ddfe7d
first test written
mishankov Oct 27, 2025
2af28b3
fix errors.Join usage
mishankov Oct 27, 2025
01d3f59
error test for SaveMigrationLogs
mishankov Oct 27, 2025
5fa1ea2
fix linter errors
mishankov Oct 27, 2025
72fa1b6
tests for GetMigrationLogs
mishankov Oct 27, 2025
6849b2e
delete RemoveMigrationLog method
mishankov Oct 27, 2025
85d84b6
tests for RevertMigrations
mishankov Oct 27, 2025
b6c09ae
remove db dependency from service
mishankov Oct 27, 2025
63acadd
add check task
mishankov Oct 27, 2025
f8f8b12
Make service constructor internal
mishankov Oct 28, 2025
64a4767
Add database integration tests with testcontainers
mishankov Oct 28, 2025
ff47636
Add macOS Docker socket override for Testcontainers
mishankov Oct 28, 2025
5777ed7
Remove macOS from CI matrix
mishankov Oct 28, 2025
7a38679
Remove Windows from CI matrix
mishankov Oct 28, 2025
1943b79
Remove unused RemoveMigrationLog method
mishankov Oct 28, 2025
8ce6475
Add database migration test cases
mishankov Oct 28, 2025
e9c7019
Track applied migrations during execution
mishankov Oct 28, 2025
6bc1bc2
Add test for migration failure with revert
mishankov Oct 28, 2025
8ec3230
Disable dupl linter for test files
mishankov Oct 28, 2025
0311e7f
Add migration log verification to database tests
mishankov Oct 28, 2025
ce37e2a
Update migration test expectations
mishankov Oct 28, 2025
c06d40a
Add table existence checks to migration tests
mishankov Oct 28, 2025
13180fe
Update migration test expectations and add rollback verification
mishankov Oct 28, 2025
4513942
Update migration test expectations and add rollback verification
mishankov Oct 28, 2025
21f07d3
Remove goconst linter from test and update migration test
mishankov Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ubuntu, windows, macos]
os: [ubuntu]
steps:
- uses: actions/checkout@v5

Expand Down
15 changes: 11 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,17 @@ linters:
rules:
- path: _test\.go
linters:
- err113
- errcheck
- gosec
- nilnil
- dupl
- err113
- errcheck
- goconst
- gosec
- nilnil

- path: database_test\.go
linters:
- paralleltest
- tparallel

formatters:
enable:
Expand Down
19 changes: 19 additions & 0 deletions Taskfile.dist.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3"

tasks:
lint:
cmds:
- golangci-lint run

fix:
cmds:
- golangci-lint run --fix

test:
cmds:
- go test -cover -v ./...

check:
deps:
- lint
- test
31 changes: 15 additions & 16 deletions auth/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
112 changes: 24 additions & 88 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ package database
import (
"context"
"fmt"
"slices"
"time"

"github.com/mishankov/platforma/log"

"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
Expand All @@ -15,116 +11,56 @@ import (
type Database struct {
*sqlx.DB
repositories map[string]any
migrators map[string]shemer
migrators map[string]migrator
repository *Repository
service *service
}

func New(connection string) (*Database, error) {
db, err := sqlx.Connect("postgres", connection)
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

repository := newRepository(db)
service := newService(repository)
return &Database{DB: db, repositories: make(map[string]any), migrators: make(map[string]migrator), repository: repository, service: service}, nil
}

func (db *Database) RegisterRepository(name string, repository any) {
db.repositories[name] = repository

if migr, ok := repository.(shemer); ok {
if migr, ok := repository.(migrator); ok {
db.migrators[name] = migr
}
}

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 {
return fmt.Errorf("failed to create migrations table: %w", err)
// Ensure that migration table exists
err := db.service.MigrateSelf(ctx)
if err != nil {
return err
}

// Select data from platforma_migrations table
var migrationsState []migrations
err := db.SelectContext(ctx, &migrationsState, "SELECT * FROM platforma_migrations")
// Get completed migrations
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 migrations) 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)
}

// Log that schema applied
if _, err := db.ExecContext(ctx, "INSERT INTO platforma_migrations (repository, timestamp) VALUES ($1, $2)", repoName, time.Now()); 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 {
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 migrations) bool {
return m.Repository == repoName && m.MigrationId.String == migration.ID
})

if migrationHasApplied {
continue
}

if _, err := db.ExecContext(ctx, migration.Up); 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
}

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)
}
}

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) {
if _, err := db.ExecContext(ctx, migration.Down); err != nil {
log.ErrorContext(ctx, "failed to rollback migration %s for repository %s", migration.ID, migration.repository)
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 {
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
}
Loading