diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3481c6..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, macos] + os: [ubuntu] steps: - uses: actions/checkout@v5 diff --git a/.golangci.yml b/.golangci.yml index 1f7a1ba..deb9368 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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: diff --git a/Taskfile.dist.yml b/Taskfile.dist.yml new file mode 100644 index 0000000..a2d84ba --- /dev/null +++ b/Taskfile.dist.yml @@ -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 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 17b23d5..55c1dcd 100644 --- a/database/database.go +++ b/database/database.go @@ -3,10 +3,6 @@ package database import ( "context" "fmt" - "slices" - "time" - - "github.com/mishankov/platforma/log" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -15,7 +11,9 @@ 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) { @@ -23,108 +21,46 @@ 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 + + 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 } diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..2e4352f --- /dev/null +++ b/database/database_test.go @@ -0,0 +1,366 @@ +package database_test + +import ( + "context" + "slices" + "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()) + } + + 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) { + 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", + }}}) + + err = db.Migrate(ctx) + 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()) + } + + // 2 = platforma_migrations + simple_repo + if len(migrationLogs) != 2 { + 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 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) { + 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()) + } + + 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 + repos + 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") + } + + _, 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, 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()) + } + }) + + 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()) + + 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) { + 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()) + + 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 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") + } + + // 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") + } + }) +} + +type simpleRepo struct { + migrations []database.Migration +} + +func (r simpleRepo) Migrations() []database.Migration { + return r.migrations +} diff --git a/database/migration.go b/database/migration.go index 8e36510..dfe5f19 100644 --- a/database/migration.go +++ b/database/migration.go @@ -1,14 +1,13 @@ package database import ( - "database/sql" "time" ) -type migrations struct { - Repository string `db:"repository"` - MigrationId sql.NullString `db:"id"` - Timestamp time.Time `db:"timestamp"` +type MigrationLog struct { + Repository string `db:"repository"` + MigrationId string `db:"id"` + Timestamp time.Time `db:"timestamp"` } type Migration struct { @@ -18,10 +17,6 @@ type Migration struct { repository string } -type Schema struct { - Queries []string -} - -type shemer interface { - Schema() ([]Migration, Schema) +type migrator interface { + Migrations() []Migration } diff --git a/database/repository.go b/database/repository.go new file mode 100644 index 0000000..7ab923d --- /dev/null +++ b/database/repository.go @@ -0,0 +1,54 @@ +package database + +import ( + "context" + "fmt" + + "github.com/jmoiron/sqlx" +) + +type Repository struct { + db *sqlx.DB +} + +func newRepository(db *sqlx.DB) *Repository { + return &Repository{db: db} +} + +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", + }} +} + +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) + } + + return migrations, nil +} + +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, log) + if err != nil { + return fmt.Errorf("failed to save 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 { + return fmt.Errorf("failed to execute query: %w", err) + } + return nil +} diff --git a/database/service.go b/database/service.go new file mode 100644 index 0000000..3d66b56 --- /dev/null +++ b/database/service.go @@ -0,0 +1,142 @@ +package database + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + "github.com/mishankov/platforma/log" +) + +type repository interface { + GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) + SaveMigrationLog(ctx context.Context, log MigrationLog) error + ExecuteQuery(ctx context.Context, query string) error + Migrations() []Migration +} + +type service struct { + repo repository +} + +func newService(repo repository) *service { + return &service{repo: repo} +} + +func (s *service) GetMigrationLogs(ctx context.Context) ([]MigrationLog, error) { + logs, err := s.repo.GetMigrationLogs(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get migration logs: %w", err) + } + return logs, nil +} + +func (s *service) SaveMigrationLog(ctx context.Context, repository, migrationId string) error { + err := s.repo.SaveMigrationLog(ctx, MigrationLog{Repository: repository, MigrationId: migrationId, Timestamp: time.Now()}) + 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 { + masterErr := error(nil) + for _, migr := range migrations { + err := s.SaveMigrationLog(ctx, migr.repository, migr.ID) + if err != nil { + masterErr = errors.Join(masterErr, err) + } + } + + return masterErr +} + +func (s *service) MigrateSelf(ctx context.Context) error { + migrations := s.repo.Migrations() + appliedMigrations := []Migration{} + migrationLogs, err := s.repo.GetMigrationLogs(ctx) + + if err != nil { + log.InfoContext(ctx, "migrations log table does not exist yet") + } + + for _, migr := range migrations { + if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { + return l.Repository == "platforma_migrations" && l.MigrationId == migr.ID + }) { + 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 + } + migr.repository = "platforma_migration" + appliedMigrations = append(appliedMigrations, migr) + } + } + + err = s.SaveMigrationLogs(ctx, appliedMigrations) + if err != nil { + log.ErrorContext(ctx, "got error(s) trying to save migration logs", "error", err.Error()) + } + + return nil +} + +func (s *service) ApplyMigration(ctx context.Context, migration Migration) error { + err := s.repo.ExecuteQuery(ctx, migration.Up) + if err != nil { + return fmt.Errorf("failed to apply migration: %w", err) + } + return nil +} + +func (s *service) ApplyMigrations(ctx context.Context, migrations []Migration, migrationLogs []MigrationLog) error { + appliedMigrations := []Migration{} + for _, migr := range migrations { + if !slices.ContainsFunc(migrationLogs, func(l MigrationLog) bool { + return l.Repository == migr.repository && l.MigrationId == migr.ID + }) { + 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) + } + } + + err := s.SaveMigrationLogs(ctx, appliedMigrations) + if err != nil { + log.ErrorContext(ctx, "got error(s) trying to save migration logs", "error", err.Error()) + } + + return nil +} + +func (s *service) RevertMigration(ctx context.Context, migration Migration) error { + err := s.repo.ExecuteQuery(ctx, migration.Down) + if err != nil { + return fmt.Errorf("failed to revert migration: %w", err) + } + return nil +} + +func (s *service) RevertMigrations(ctx context.Context, migrations []Migration) error { + masterErr := error(nil) + for _, migr := range slices.Backward(migrations) { + err := s.RevertMigration(ctx, migr) + if err != nil { + masterErr = errors.Join(masterErr, fmt.Errorf("failed to revert migration %s: %w", migr.ID, err)) + } + } + + return masterErr +} 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= 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) {