Migration

/app/lib/database/migrate/migrate.go (2.8 KB)

  1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package migrate

import (
"context"

"github.com/jmoiron/sqlx"
"github.com/pkg/errors"

"{{{ .Package }}}/app/lib/database"
"{{{ .Package }}}/app/lib/telemetry"
"{{{ .Package }}}/app/util"
)

func Migrate(ctx context.Context, s *database.Service, logger util.Logger) error {
logger = logger.With("svc", "migrate")
ctx, span, logger := telemetry.StartSpan(ctx, "database:migrate", logger)
defer span.Complete()

err := createMigrationTableIfNeeded(ctx, s, nil, logger)
if err != nil {
return errors.Wrap(err, "unable to create migration table")
}

tx, err := s.StartTransaction(logger)
if err != nil {
return errors.Wrap(err, "unable to start transaction")
}
defer func() {
_ = tx.Rollback()
}()

maxIdx := maxMigrationIdx(ctx, s, tx, logger)

if len(databaseMigrations) > maxIdx+1 {
c := len(databaseMigrations) - maxIdx
logger.Infof("applying [%s]...", util.StringPlural(c, "migration"))
}

for i, file := range databaseMigrations {
err = run(ctx, maxIdx, i, file, s, tx, logger)
if err != nil {
return errors.Wrapf(err, "error running database migration [%s]", file.Title)
}
}

if err := tx.Commit(); err != nil {
return err
}

logger.Infof("verified [%s]", util.StringPlural(maxIdx, "migration"))
return nil
}

func run(ctx context.Context, maxIdx int, i int, file *MigrationFile, s *database.Service, tx *sqlx.Tx, logger util.Logger) error {
idx := i + 1
switch {
case idx == maxIdx:
m := getMigrationByIdx(ctx, s, maxIdx, tx, logger)
if m == nil {
return nil
}
if m.Title != file.Title {
logger.Infof("migration [%d] name has changed from [%s] to [%s]", idx, m.Title, file.Title)
err := removeMigrationByIdx(ctx, s, idx, tx, logger)
if err != nil {
return err
}
err = applyMigration(ctx, s, idx, file, tx, logger)
if err != nil {
return err
}
return nil
}
nc := file.Content
if nc != m.Src {
logger.Infof("migration [%d:%s] content has changed from [%dB] to [%dB]", idx, file.Title, len(nc), len(m.Src))
err := removeMigrationByIdx(ctx, s, idx, tx, logger)
if err != nil {
return err
}
err = applyMigration(ctx, s, idx, file, tx, logger)
if err != nil {
return err
}
}
case idx > maxIdx:
err := applyMigration(ctx, s, idx, file, tx, logger)
if err != nil {
return err
}
default:
// noop
}
return nil
}

func applyMigration(ctx context.Context, s *database.Service, idx int, file *MigrationFile, tx *sqlx.Tx, logger util.Logger) error {
logger.Infof("applying database migration [%d]: %s", idx, file.Title)
sql, err := exec(ctx, file, s, tx, logger)
if err != nil {
return err
}
m := &Migration{Idx: idx, Title: file.Title, Src: sql, Created: util.TimeCurrent()}
return newMigration(ctx, s, m, tx, logger)
}