Overview
In my previous job and my current job, DB migrations seem to be a constant in any situation. In fact, in my case, I didn’t utilize a tool until now, but rather wrote ad hoc code or scripts. In today’s post, I’d like to introduce Goose, a DB-related migration tool for Golang.
What is DB migration?
DB migration has many different meanings and situations. When we say we’re doing a DB migration, we can think of the following situations. The exact content may vary depending on the context.
- Performance / system upgrade improvements
- DB migration: moving to a lower cost instance or a different type of DB
- Consolidation/decoupling: bringing multiple data sources together in one place or vice versa
- Enhancements: Additions/modifications or deletions to existing datasets due to system changes.
DB migration often refers to a number of different tasks that come with a DB. The Goose tool we’re going to talk about today is related to enhancements.
In the real world, you might be updating data for specific users, dropping columns that are no longer in use, or any of these types of things.
Why did we use Goose?
Goose is a database migration tool written in the Go language. There are many other DB migration tools out there, but we were working on a project with Golang and didn’t want to create another dependency for the migration tool.
As mentioned above, I had to choose between migration tools written in Golang, but I chose the one that was the most intuitive and had the smallest learning curve.
From a maintenance perspective, I also considered whether it had a history of commits within the last 3 months and issues were still being managed.
Installing Goose
Let’s start by simply installing Goose.
- On macOS
1brew install goose
2```bash brew install goose
3
4- Linux
5
6```bash
7curl -fsSL \
8 https://raw.githubusercontent.com/pressly/goose/master/install.sh |\
9 GOOSE_INSTALL=$HOME/.goose sh -s v3.5.0
- Go Install
1go install github.com/pressly/goose/v3/cmd/goose@latest
- Build directly from code
1git clone https://github.com/pressly/goose
2cd goose
3go mod tidy
4go build -o goose ./cmd/goose
5
6./goose --version
7# goose version:(devel)
See also: https://pressly.github.io/goose/installation/
You’ll want to install it for the environment and context in which you want to run it. The following instructions will cover the v3 version, so please install it after that version. (Latest version as of this writing: v3.18.0)
After installing, execute the goose command to display the help page like below.
1goose ok
2Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
3
4or
5
6Set environment key
7goose_driver=driver
8goose_dbstring=dbstring
9
10Usage: goose [OPTIONS] COMMAND
11
12Drivers:
13 postgres
14 mysql
15 sqlite3
16 mssql
17 redshift
18 tidb
19 clickhouse
20 vertica
21 ydb
22 turso
23...omit
Goose supports the following drivers (I only know the top 5, the rest are new to me)
- postgres
- mysql
- sqlite3
- mssql
- redshift
- tidb
- clickhouse
- vertica
- ydb
- turso
Hands-on
Goose can perform migrations in three different ways.
- SQL Migrations
- Embedded sql migrations
- Go Migrations
For each case, we will explain in detail with example code. For the sake of speed, we will use SQLite only.
**SQLite test data
1> sqlite3 example.db
2Enter ".help" for usage hints.
3sqlite> CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, name TEXT);
4sqlite> INSERT INTO users(name) VALUES('John Doe');
5sqlite> INSERT INTO users(name) VALUES('Mike Cho');
6sqlite> .exit
SQL Migration
- Initialize the Goose script
1> GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose create init sql
22024/02/27 21:01:39 Created new file: 20240227120139_init.sql
The above command creates a file in UnixTime format. The file is organized as shown below.
1-- +goose Up
2-- +goose StatementBegin
3-- +goose StatementEnd
4
5-- +goose Down
6-- +goose StatementBegin
7-- +goose StatementEnd
Goose commands must have the comments +goose Up
+goose Down
. The SQL statements below those comments are performed under the transaction, and inside +goose StatementBegin
and +goose StatementEnd
, you can perform complex queries. (ex. PL/pgSQL)
Below is the SQL syntax to delete and recover specific data.
1-- +goose Up
2DELETE FROM users WHERE name = 'John Doe';
3
4-- +goose Down
5INSERT INTO users(name) VALUES('John Doe');
- Check status
1> GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose status
22024/02/27 21:01:41 Applied At Migration
32024/02/27 21:01:41 =======================================
42024/02/27 21:01:41 Pending -- 20240227120139_init.sql
- Performing Migration
1> GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose up
22024/02/27 21:02:04 OK 20240227120139_init.sql (28.55ms)
32024/02/27 21:02:04 goose: successfully migrated database to version: 20240227120139
After performing this migration, the row named ‘John Doe’ will be deleted.
- Verify the migration result
1> GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose status
22024/02/27 21:17:44 Applied At Migration
32024/02/27 21:17:44 =======================================
42024/02/27 21:17:44 Tue Feb 27 12:02:39 2024 -- 20240227120139_init.sql
- Cancel migration (rollback)
1> GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose down
22024/02/27 21:02:11 OK 20240227120139_init.sql (28.35ms)
1GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./example.db goose status ok | %
22024/02/27 21:02:17 Applied At Migration
32024/02/27 21:02:17 =======================================
42024/02/27 21:02:17 Pending -- 20240227120139_init.sql
Embedded sql migrations
In this case, SQL statements are imported using go:embed and migrations are performed using the goose package.
- Installing
- main.go
1package main
2
3import (
4 "database/sql"
5 "embed"
6 "log"
7
8 _ "github.com/mattn/go-sqlite3" // import SQLite driver
9 "github.com/pressly/goose/v3"
10)
11
12//go:embed 20240227120139_init.sql
13var embedMigrations embed.FS
14
15func main() {
16 // Set the path to the SQLite database file
17 db, err := sql.Open("sqlite3", "example.db")
18 if err != nil {
19 log.Fatalf("Failed to open database: %v", err)
20 }
21 } defer db.Close()
22
23 // Set the file system embedded in Goose
24 goose.SetBaseFS(embedMigrations)
25
26 if err := goose.SetDialect("sqlite3"); err != nil {
27 log.Fatalf("Failed to set dialect: %v", err)
28 }
29
30 // Run the migration
31 if err := goose.Up(db, "."); err != nil {
32 log.Fatalf("Failed to run migrations: %v", err)
33 }
34}
- 結果
12024/02/27 21:27:51 OK 20240227120139_init.sql (21.87ms)
22024/02/27 21:27:51 goose: successfully migrated database to version: 20240227120139
Go Migration
In this case, the migration control is in the actual internal code, not the script. The two examples above were SQL-based migrations, so we needed SQL files, but in go, we don’t need SQL syntax because we do the query statements directly internally.
Instead, we need to keep a history, so we use the filename of the perform as the version. The filename must start with a number and we use an underscore (_) as a separator.
20240227125030_migration.go
- main.go
1// 20240227125030_migration.go
2package main
3
4import (
5 "context"
6 "database/sql"
7 "log"
8
9 _ "github.com/mattn/go-sqlite3"
10 "github.com/pressly/goose/v3"
11)
12
13func init() {
14 // Register the migration function
15 goose.AddMigrationContext(Up, Down)
16}
17
18func main() {
19 db, err := sql.Open("sqlite3", "example.db")
20 if err != nil {
21 log.Fatal("Cannot open database", err)
22 }
23 } defer db.Close()
24
25 // Set up Goose
26 goose.SetDialect("sqlite3")
27
28 // Run all migrations
29 if err := goose.Up(db, "."); err != nil {
30 log.Fatalf("Goose up error: %v", err)
31 }
32}
33
34} func Up(ctx context.Context, tx *sql.Tx) error {
35 if _, err := tx.Exec("DELETE FROM users WHERE name = 'John Doe';"); err != nil {
36 return err
37 }
38
39 } return nil
40}
41
42func Down(ctx context.Context, tx *sql.Tx) error {
43 if _, err := tx.Exec("INSERT INTO users(name) VALUES('John Doe');"); err != nil {
44 return err
45 }
46
47 return nil
48}
Executing the code above will generate a version of 20240227125030.
12024/02/27 21:54:07 OK 20240227125030_main.go (21.19ms)
22024/02/27 21:54:07 goose: successfully migrated database to version: 20240227125030
Summary
In this post, I introduced Goose, one of the Go Migration libraries. I found it intuitive and understood it well enough just by looking at the README. I think it’s enough for those who don’t expect to use it in a complex way, but if you want more advanced features, you can try other libraries or solutions.