Browse Source

Add --migrate command

This runs database migrations and obviates the need for manually running
changes.

Ref T509
pull/68/head
Matt Baer 5 years ago
parent
commit
47b2155f92
3 changed files with 196 additions and 0 deletions
  1. +14
    -0
      app.go
  2. +66
    -0
      migrations/drivers.go
  3. +116
    -0
      migrations/migrations.go

+ 14
- 0
app.go View File

@@ -36,6 +36,7 @@ import (
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/migrations"
"github.com/writeas/writefreely/page"
)

@@ -193,6 +194,7 @@ func Serve() {
doConfig := flag.Bool("config", false, "Run the configuration process")
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
createSchema := flag.Bool("init-db", false, "Initialize app database")
migrate := flag.Bool("migrate", false, "Migrate the database")
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
@@ -312,6 +314,18 @@ func Serve() {
}
log.Info("Success.")
os.Exit(0)
} else if *migrate {
loadConfig(app)
connectToDatabase(app)
defer shutdown(app)

err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil {
log.Error("migrate: %s", err)
os.Exit(1)
}

os.Exit(0)
}

log.Info("Initializing...")


+ 66
- 0
migrations/drivers.go View File

@@ -0,0 +1,66 @@
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/

package migrations

import (
"fmt"
)

// TODO: use now() from writefreely pkg
func (db *datastore) now() string {
if db.driverName == driverSQLite {
return "strftime('%Y-%m-%d %H:%M:%S','now')"
}
return "NOW()"
}

func (db *datastore) typeInt() string {
if db.driverName == driverSQLite {
return "INTEGER"
}
return "INT"
}

func (db *datastore) typeSmallInt() string {
if db.driverName == driverSQLite {
return "INTEGER"
}
return "SMALLINT"
}

func (db *datastore) typeText() string {
return "TEXT"
}

func (db *datastore) typeChar(l int) string {
if db.driverName == driverSQLite {
return "TEXT"
}
return fmt.Sprintf("CHAR(%d)", l)
}

func (db *datastore) typeBool() string {
if db.driverName == driverSQLite {
return "INTEGER"
}
return "TINYINT(1)"
}

func (db *datastore) typeDateTime() string {
return "DATETIME"
}

func (db *datastore) engine() string {
if db.driverName == driverSQLite {
return ""
}
return " ENGINE = InnoDB"
}

+ 116
- 0
migrations/migrations.go View File

@@ -0,0 +1,116 @@
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/

// Package migrations contains database migrations for WriteFreely
package migrations

import (
"database/sql"
"github.com/writeas/web-core/log"
)

// TODO: refactor to use the datastore struct from writefreely pkg
type datastore struct {
*sql.DB
driverName string
}

func NewDatastore(db *sql.DB, dn string) *datastore {
return &datastore{db, dn}
}

// TODO: use these consts from writefreely pkg
const (
driverMySQL = "mysql"
driverSQLite = "sqlite3"
)

type Migration interface {
Description() string
Migrate(db *datastore) error
}

type migration struct {
description string
migrate func(db *datastore) error
}

func New(d string, fn func(db *datastore) error) Migration {
return &migration{d, fn}
}

func (m *migration) Description() string {
return m.description
}

func (m *migration) Migrate(db *datastore) error {
return m.migrate(db)
}

var migrations = []Migration{}

func Migrate(db *datastore) error {
var version int
var err error
if db.tableExists("appmigrations") {
err = db.QueryRow("SELECT MAX(version) FROM appmigrations").Scan(&version)
} else {
log.Info("Initializing appmigrations table...")
version = 0
_, err = db.Exec(`CREATE TABLE appmigrations (
version ` + db.typeInt() + ` NOT NULL,
migrated ` + db.typeDateTime() + ` NOT NULL,
result ` + db.typeText() + ` NOT NULL
) ` + db.engine() + `;`)
if err != nil {
return err
}
}

if len(migrations[version:]) > 0 {
for i, m := range migrations[version:] {
curVer := version + i + 1
log.Info("Migrating to V%d: %s", curVer, m.Description())
err = m.Migrate(db)
if err != nil {
return err
}

// Update migrations table
_, err = db.Exec("INSERT INTO appmigrations (version, migrated, result) VALUES (?, "+db.now()+", ?)", curVer, "")
if err != nil {
return err
}
}
} else {
log.Info("Database up-to-date. No migrations to run.")
}

return nil
}

func (db *datastore) tableExists(t string) bool {
var dummy string
var err error
if db.driverName == driverSQLite {
err = db.QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", t).Scan(&dummy)
} else {
err = db.QueryRow("SHOW TABLES LIKE ?", t).Scan(&dummy)
}
switch {
case err == sql.ErrNoRows:
return false
case err != nil:
log.Error("Couldn't SHOW TABLES: %v", err)
return false
}

return true
}

Loading…
Cancel
Save