Add --migrate command
This runs database migrations and obviates the need for manually running changes. Ref T509
This commit is contained in:
parent
6c2bd8031a
commit
47b2155f92
14
app.go
14
app.go
|
@ -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")
|
||||
|
@ -311,6 +313,18 @@ func Serve() {
|
|||
os.Exit(1)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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…
Reference in New Issue