This runs database migrations and obviates the need for manually running changes. Ref T509pull/68/head
@@ -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...") | |||
@@ -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 | |||
} |