Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
version: "2"

linters:
# consider to add: cyclop
# find sane settings for revive
default: none
enable:
- bodyclose
Expand Down Expand Up @@ -35,6 +33,7 @@ linters:
- paralleltest
- perfsprint
- prealloc
- revive
- staticcheck
- tagalign
- tagliatelle
Expand Down Expand Up @@ -63,6 +62,10 @@ linters:
json: camel
exclusions:
rules:
- path-except: "database/*"
linters:
- revive

- path: _test\.go
linters:
- dupl
Expand Down
17 changes: 11 additions & 6 deletions database/database.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
// Package database provides database connection and migration functionality.
package database

import (
"context"
"fmt"

"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
_ "github.com/lib/pq" // PostgreSQL driver
)

// Database represents a database connection with migration capabilities.
type Database struct {
*sqlx.DB
repositories map[string]any
migrators map[string]migrator
repository *Repository
service *service
}

// New creates a new Database instance with the given connection string.
func New(connection string) (*Database, error) {
db, err := sqlx.Connect("postgres", connection)
if err != nil {
Expand All @@ -24,9 +26,11 @@ func New(connection string) (*Database, error) {

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

// RegisterRepository registers a repository in the database.
// If repository implements migrator interface, it will migrate when `Migrate` is called.
func (db *Database) RegisterRepository(name string, repository any) {
db.repositories[name] = repository

Expand All @@ -35,15 +39,16 @@ func (db *Database) RegisterRepository(name string, repository any) {
}
}

// Migrate runs all pending migrations for registered repositories.
func (db *Database) Migrate(ctx context.Context) error {
// Ensure that migration table exists
err := db.service.MigrateSelf(ctx)
err := db.service.migrateSelf(ctx)
if err != nil {
return err
}

// Get completed migrations
migrationLogs, err := db.service.GetMigrationLogs(ctx)
migrationLogs, err := db.service.getMigrationLogs(ctx)
if err != nil {
return fmt.Errorf("failed to select migrations state: %w", err)
}
Expand All @@ -57,7 +62,7 @@ func (db *Database) Migrate(ctx context.Context) error {
}
}

err = db.service.ApplyMigrations(ctx, migrations, migrationLogs)
err = db.service.applyMigrations(ctx, migrations, migrationLogs)
if err != nil {
return err
}
Expand Down
63 changes: 35 additions & 28 deletions database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"slices"
"testing"
"time"

"github.com/mishankov/platforma/database"
"github.com/testcontainers/testcontainers-go/modules/postgres"
Expand Down Expand Up @@ -59,7 +60,7 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to migrate database: %s", err.Error())
}

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -73,8 +74,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository)
}

if migrationLogs[0].MigrationId != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId)
if migrationLogs[0].MigrationID != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID)
}
})

Expand Down Expand Up @@ -109,7 +110,7 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to migrate database: %s", err.Error())
}

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -123,8 +124,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository)
}

if migrationLogs[0].MigrationId != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId)
if migrationLogs[0].MigrationID != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID)
}
})

Expand Down Expand Up @@ -156,7 +157,7 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to migrate database: %s", err.Error())
}

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -167,8 +168,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected 2 migrations, got: %d", len(migrationLogs))
}

if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "some_repo" && log.MigrationId == "init"
if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "some_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to contain init migration for some_repo")
}
Expand Down Expand Up @@ -213,7 +214,7 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to migrate database: %s", err.Error())
}

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -224,8 +225,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected 3 migrations, got: %d", len(migrationLogs))
}

if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "some_repo" && log.MigrationId == "init"
if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "some_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to contain init migration for some_repo")
}
Expand All @@ -235,8 +236,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected no errors, got: %s", err.Error())
}

if !slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "other_repo" && log.MigrationId == "init"
if !slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "other_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to contain init migration for other_repo, but only got: %s", migrationLogs)
}
Expand Down Expand Up @@ -286,7 +287,7 @@ func TestMigrate(t *testing.T) {
}
t.Logf("migration error: %s", err.Error())

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -300,13 +301,13 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository)
}

if migrationLogs[0].MigrationId != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId)
if migrationLogs[0].MigrationID != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID)
}

// because migration should be reverted
if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "some_repo" && log.MigrationId == "init"
if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "some_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to not contain init migration for some_repo")
}
Expand All @@ -317,8 +318,8 @@ func TestMigrate(t *testing.T) {
}

// because migration should be reverted
if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "other_repo" && log.MigrationId == "init"
if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "other_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to not contain init migration for other_repo, but only got: %s", migrationLogs)
}
Expand Down Expand Up @@ -368,7 +369,7 @@ func TestMigrate(t *testing.T) {
}
t.Logf("migration error: %s", err.Error())

var migrationLogs []database.MigrationLog
var migrationLogs []migrationLog
err = db.SelectContext(ctx, &migrationLogs, "SELECT * FROM platforma_migrations")
if err != nil {
t.Fatalf("expected no errors, got: %s", err.Error())
Expand All @@ -382,20 +383,20 @@ func TestMigrate(t *testing.T) {
t.Fatalf("expected repository to be platforma_migration, got: %s", migrationLogs[0].Repository)
}

if migrationLogs[0].MigrationId != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationId)
if migrationLogs[0].MigrationID != "init" {
t.Fatalf("expected migration id to be init, got: %s", migrationLogs[0].MigrationID)
}

// because migration should be reverted or not even attempted
if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "some_repo" && log.MigrationId == "init"
if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "some_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to not contain init migration for some_repo")
}

// because migration should be reverted
if slices.ContainsFunc(migrationLogs, func(log database.MigrationLog) bool {
return log.Repository == "other_repo" && log.MigrationId == "init"
if slices.ContainsFunc(migrationLogs, func(log migrationLog) bool {
return log.Repository == "other_repo" && log.MigrationID == "init"
}) {
t.Fatalf("expected migration log to not contain init migration for other_repo, but only got: %s", migrationLogs)
}
Expand All @@ -407,6 +408,12 @@ func TestMigrate(t *testing.T) {
})
}

type migrationLog struct {
Repository string `db:"repository"`
MigrationID string `db:"id"`
Timestamp time.Time `db:"timestamp"`
}

type simpleRepo struct {
migrations []database.Migration
}
Expand Down
5 changes: 3 additions & 2 deletions database/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import (
"time"
)

type MigrationLog struct {
type migrationLog struct {
Repository string `db:"repository"`
MigrationId string `db:"id"`
MigrationID string `db:"id"`
Timestamp time.Time `db:"timestamp"`
}

// Migration represents a database migration with up and down SQL statements.
type Migration struct {
ID string
Up string
Expand Down
16 changes: 8 additions & 8 deletions database/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,24 @@ 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)",
Down: "DROP TABLE platforma_migrations",
}}
}

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)
Expand All @@ -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)
Expand All @@ -45,7 +45,7 @@ func (r *Repository) SaveMigrationLog(ctx context.Context, log MigrationLog) err
return nil
}

func (r *Repository) ExecuteQuery(ctx context.Context, query string) error {
func (r *repository) executeQuery(ctx context.Context, query string) error {
_, err := r.db.ExecContext(ctx, query)
if err != nil {
return fmt.Errorf("failed to execute query: %w", err)
Expand Down
Loading