upgrade.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/jmoiron/sqlx"
  6. "github.com/knadh/koanf"
  7. "github.com/knadh/listmonk/internal/migrations"
  8. "github.com/knadh/stuffbin"
  9. "github.com/lib/pq"
  10. "golang.org/x/mod/semver"
  11. )
  12. // migFunc represents a migration function for a particular version.
  13. // fn (generally) executes database migrations and additionally
  14. // takes the filesystem and config objects in case there are additional bits
  15. // of logic to be performed before executing upgrades. fn is idempotent.
  16. type migFunc struct {
  17. version string
  18. fn func(*sqlx.DB, stuffbin.FileSystem, *koanf.Koanf) error
  19. }
  20. // migList is the list of available migList ordered by the semver.
  21. // Each migration is a Go file in internal/migrations named after the semver.
  22. // The functions are named as: v0.7.0 => migrations.V0_7_0() and are idempotent.
  23. var migList = []migFunc{
  24. {"v0.4.0", migrations.V0_4_0},
  25. {"v0.7.0", migrations.V0_7_0},
  26. {"v0.8.0", migrations.V0_8_0},
  27. {"v0.9.0", migrations.V0_9_0},
  28. {"v1.0.0", migrations.V1_0_0},
  29. {"v2.0.0", migrations.V2_0_0},
  30. }
  31. // upgrade upgrades the database to the current version by running SQL migration files
  32. // for all version from the last known version to the current one.
  33. func upgrade(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
  34. if prompt {
  35. var ok string
  36. fmt.Printf("** IMPORTANT: Take a backup of the database before upgrading.\n")
  37. fmt.Print("continue (y/n)? ")
  38. if _, err := fmt.Scanf("%s", &ok); err != nil {
  39. lo.Fatalf("error reading value from terminal: %v", err)
  40. }
  41. if strings.ToLower(ok) != "y" {
  42. fmt.Println("upgrade cancelled")
  43. return
  44. }
  45. }
  46. _, toRun, err := getPendingMigrations(db)
  47. if err != nil {
  48. lo.Fatalf("error checking migrations: %v", err)
  49. }
  50. // No migrations to run.
  51. if len(toRun) == 0 {
  52. lo.Printf("no upgrades to run. Database is up to date.")
  53. return
  54. }
  55. // Execute migrations in succession.
  56. for _, m := range toRun {
  57. lo.Printf("running migration %s", m.version)
  58. if err := m.fn(db, fs, ko); err != nil {
  59. lo.Fatalf("error running migration %s: %v", m.version, err)
  60. }
  61. // Record the migration version in the settings table. There was no
  62. // settings table until v0.7.0, so ignore the no-table errors.
  63. if err := recordMigrationVersion(m.version, db); err != nil {
  64. if isTableNotExistErr(err) {
  65. continue
  66. }
  67. lo.Fatalf("error recording migration version %s: %v", m.version, err)
  68. }
  69. }
  70. lo.Printf("upgrade complete")
  71. }
  72. // checkUpgrade checks if the current database schema matches the expected
  73. // binary version.
  74. func checkUpgrade(db *sqlx.DB) {
  75. lastVer, toRun, err := getPendingMigrations(db)
  76. if err != nil {
  77. lo.Fatalf("error checking migrations: %v", err)
  78. }
  79. // No migrations to run.
  80. if len(toRun) == 0 {
  81. return
  82. }
  83. var vers []string
  84. for _, m := range toRun {
  85. vers = append(vers, m.version)
  86. }
  87. lo.Fatalf(`there are %d pending database upgrade(s): %v. The last upgrade was %s. Backup the database and run listmonk --upgrade`,
  88. len(toRun), vers, lastVer)
  89. }
  90. // getPendingMigrations gets the pending migrations by comparing the last
  91. // recorded migration in the DB against all migrations listed in `migrations`.
  92. func getPendingMigrations(db *sqlx.DB) (string, []migFunc, error) {
  93. lastVer, err := getLastMigrationVersion()
  94. if err != nil {
  95. return "", nil, err
  96. }
  97. // Iterate through the migration versions and get everything above the last
  98. // last upgraded semver.
  99. var toRun []migFunc
  100. for i, m := range migList {
  101. if semver.Compare(m.version, lastVer) > 0 {
  102. toRun = migList[i:]
  103. break
  104. }
  105. }
  106. return lastVer, toRun, nil
  107. }
  108. // getLastMigrationVersion returns the last migration semver recorded in the DB.
  109. // If there isn't any, `v0.0.0` is returned.
  110. func getLastMigrationVersion() (string, error) {
  111. var v string
  112. if err := db.Get(&v, `
  113. SELECT COALESCE(
  114. (SELECT value->>-1 FROM settings WHERE key='migrations'),
  115. 'v0.0.0')`); err != nil {
  116. if isTableNotExistErr(err) {
  117. return "v0.0.0", nil
  118. }
  119. return v, err
  120. }
  121. return v, nil
  122. }
  123. // isPqNoTableErr checks if the given error represents a Postgres/pq
  124. // "table does not exist" error.
  125. func isTableNotExistErr(err error) bool {
  126. if p, ok := err.(*pq.Error); ok {
  127. // `settings` table does not exist. It was introduced in v0.7.0.
  128. if p.Code == "42P01" {
  129. return true
  130. }
  131. }
  132. return false
  133. }