aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorKegsay <kegan@matrix.org>2020-09-10 15:10:32 +0100
committerGitHub <noreply@github.com>2020-09-10 15:10:32 +0100
commit95d7e2336d9ae96186a097ee72e6b4c279508679 (patch)
tree77bb4869afb329ff8680a11b490d7629e2af4eb5 /cmd
parent39507bacc3dbfc532e0d69b42957c87f27af4c77 (diff)
Add support for database migrations (#1416)
* Add support for database migrations Closes #1246 This PR does NOT add any migrations as an example. I have manually tested that the library works with SQL and Go based upgrades correctly. Documentation should be sufficient for devs to add migrations. * Clarifications * Linting
Diffstat (limited to 'cmd')
-rw-r--r--cmd/goose/README.md107
-rw-r--r--cmd/goose/main.go98
2 files changed, 205 insertions, 0 deletions
diff --git a/cmd/goose/README.md b/cmd/goose/README.md
new file mode 100644
index 00000000..c7f085d8
--- /dev/null
+++ b/cmd/goose/README.md
@@ -0,0 +1,107 @@
+## Database migrations
+
+We use [goose](https://github.com/pressly/goose) to handle database migrations. This allows us to execute
+both SQL deltas (e.g `ALTER TABLE ...`) as well as manipulate data in the database in Go using Go functions.
+
+To run a migration, the `goose` binary in this directory needs to be built:
+```
+$ go build ./cmd/goose
+```
+
+This binary allows Dendrite databases to be upgraded and downgraded. Sample usage for upgrading the roomserver database:
+
+```
+# for sqlite
+$ ./goose -dir roomserver/storage/sqlite3/deltas sqlite3 ./roomserver.db up
+
+# for postgres
+$ ./goose -dir roomserver/storage/postgres/deltas postgres "user=dendrite dbname=dendrite sslmode=disable" up
+```
+
+For a full list of options, including rollbacks, see https://github.com/pressly/goose or use `goose` with no args.
+
+
+### Rationale
+
+Dendrite creates tables on startup using `CREATE TABLE IF NOT EXISTS`, so you might think that we should also
+apply version upgrades on startup as well. This is convenient and doesn't involve an additional binary to run
+which complicates upgrades. However, combining the upgrade mechanism and the server binary makes it difficult
+to handle rollbacks. Firstly, how do you specify you wish to rollback? We would have to add additional flags
+to the main server binary to say "rollback to version X". Secondly, if you roll back the server binary from
+version 5 to version 4, the version 4 binary doesn't know how to rollback the database from version 5 to
+version 4! For these reasons, we prefer to have a separate "upgrade" binary which is run for database upgrades.
+Rather than roll-our-own migration tool, we decided to use [goose](https://github.com/pressly/goose) as it supports
+complex migrations in Go code in addition to just executing SQL deltas. Other alternatives like
+`github.com/golang-migrate/migrate` [do not support](https://github.com/golang-migrate/migrate/issues/15) these
+kinds of complex migrations.
+
+### Adding new deltas
+
+You can add `.sql` or `.go` files manually or you can use goose to create them for you.
+
+If you only want to add a SQL delta then run:
+
+```
+$ ./goose -dir serverkeyapi/storage/sqlite3/deltas sqlite3 ./foo.db create new_col sql
+2020/09/09 14:37:43 Created new file: serverkeyapi/storage/sqlite3/deltas/20200909143743_new_col.sql
+```
+
+In this case, the version number is `20200909143743`. The important thing is that it is always increasing.
+
+Then add up/downgrade SQL commands to the created file which looks like:
+```sql
+-- +goose Up
+-- +goose StatementBegin
+SELECT 'up SQL query';
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+SELECT 'down SQL query';
+-- +goose StatementEnd
+
+```
+You __must__ keep the `+goose` annotations. You'll need to repeat this process for Postgres.
+
+For complex Go migrations:
+
+```
+$ ./goose -dir serverkeyapi/storage/sqlite3/deltas sqlite3 ./foo.db create complex_update go
+2020/09/09 14:40:38 Created new file: serverkeyapi/storage/sqlite3/deltas/20200909144038_complex_update.go
+```
+
+Then modify the created `.go` file which looks like:
+
+```go
+package migrations
+
+import (
+ "database/sql"
+ "fmt"
+
+ "github.com/pressly/goose"
+)
+
+func init() {
+ goose.AddMigration(upComplexUpdate, downComplexUpdate)
+}
+
+func upComplexUpdate(tx *sql.Tx) error {
+ // This code is executed when the migration is applied.
+ return nil
+}
+
+func downComplexUpdate(tx *sql.Tx) error {
+ // This code is executed when the migration is rolled back.
+ return nil
+}
+
+```
+
+You __must__ import the package in `/cmd/goose/main.go` so `func init()` gets called.
+
+
+#### Database limitations
+
+- SQLite3 does NOT support `ALTER TABLE table_name DROP COLUMN` - you would have to rename the column or drop the table
+ entirely and recreate it.
diff --git a/cmd/goose/main.go b/cmd/goose/main.go
new file mode 100644
index 00000000..ef3942d9
--- /dev/null
+++ b/cmd/goose/main.go
@@ -0,0 +1,98 @@
+// This is custom goose binary
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+
+ // Example complex Go migration import:
+ // _ "github.com/matrix-org/dendrite/serverkeyapi/storage/postgres/deltas"
+ "github.com/pressly/goose"
+
+ _ "github.com/lib/pq"
+ _ "github.com/mattn/go-sqlite3"
+)
+
+var (
+ flags = flag.NewFlagSet("goose", flag.ExitOnError)
+ dir = flags.String("dir", ".", "directory with migration files")
+)
+
+func main() {
+ err := flags.Parse(os.Args[1:])
+ if err != nil {
+ panic(err.Error())
+ }
+ args := flags.Args()
+
+ if len(args) < 3 {
+ fmt.Println(
+ `Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
+
+Drivers:
+ postgres
+ sqlite3
+
+Examples:
+ goose -d roomserver/storage/sqlite3/deltas sqlite3 ./roomserver.db status
+ goose -d roomserver/storage/sqlite3/deltas sqlite3 ./roomserver.db up
+
+ goose -d roomserver/storage/postgres/deltas postgres "user=dendrite dbname=dendrite sslmode=disable" status
+
+Options:
+
+ -dir string
+ directory with migration files (default ".")
+ -table string
+ migrations table name (default "goose_db_version")
+ -h print help
+ -v enable verbose mode
+ -version
+ print version
+
+Commands:
+ up Migrate the DB to the most recent version available
+ up-by-one Migrate the DB up by 1
+ up-to VERSION Migrate the DB to a specific VERSION
+ down Roll back the version by 1
+ down-to VERSION Roll back to a specific VERSION
+ redo Re-run the latest migration
+ reset Roll back all migrations
+ status Dump the migration status for the current DB
+ version Print the current version of the database
+ create NAME [sql|go] Creates new migration file with the current timestamp
+ fix Apply sequential ordering to migrations`,
+ )
+ return
+ }
+
+ engine := args[0]
+ if engine != "sqlite3" && engine != "postgres" {
+ fmt.Println("engine must be one of 'sqlite3' or 'postgres'")
+ return
+ }
+ dbstring, command := args[1], args[2]
+
+ db, err := goose.OpenDBWithDriver(engine, dbstring)
+ if err != nil {
+ log.Fatalf("goose: failed to open DB: %v\n", err)
+ }
+
+ defer func() {
+ if err := db.Close(); err != nil {
+ log.Fatalf("goose: failed to close DB: %v\n", err)
+ }
+ }()
+
+ arguments := []string{}
+ if len(args) > 3 {
+ arguments = append(arguments, args[3:]...)
+ }
+
+ if err := goose.Run(command, db, *dir, arguments...); err != nil {
+ log.Fatalf("goose %v: %v", command, err)
+ }
+}