Migrations at start-up: MySQL and TimescaleDB
Every file of the MySQL and TimescaleDB versions side by side, with the lines that differ marked.
Configuration
sgl.yamldiffers
MySQL
TimescaleDB
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: harbour4
- name: harbour5
dialect: mysql5
dialect: postgres6
migrations: db/migrations6
migrations: db/migrations7
queries: db/queries7
queries: db/queries8
go:8
go:9
package: store9
package: store10
out: internal/store10
out: internal/store11
driver: database/sql12
migrate:13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_boats.up.sqldiffers
MySQL
TimescaleDB
1
CREATE TABLE boats (1
CREATE TABLE boats (2
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
name VARCHAR(50) NOT NULL3
name TEXT NOT NULL4
);4
);db/migrations/0001_boats.down.sqlidentical
MySQL
TimescaleDB
1
DROP TABLE boats;1
DROP TABLE boats;db/migrations/0002_boat_home_port.up.sqldiffers
MySQL
TimescaleDB
1
ALTER TABLE boats ADD COLUMN home_port VARCHAR(50) NOT NULL DEFAULT 'Padstow';1
ALTER TABLE boats ADD COLUMN home_port TEXT NOT NULL DEFAULT 'Padstow';db/migrations/0002_boat_home_port.down.sqlidentical
MySQL
TimescaleDB
1
ALTER TABLE boats DROP COLUMN home_port;1
ALTER TABLE boats DROP COLUMN home_port;Queries
AddBoatdiffers
MySQL · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(AddBoat, exec)1
-- sgl.query(AddBoat, exec)2
INSERT INTO boats (name, home_port) VALUES (?, ?);2
INSERT INTO boats (name, home_port) VALUES ($1, $2);ListBoatsidentical
MySQL · db/queries/boats.sql
TimescaleDB · db/queries/boats.sql
1
-- sgl.query(ListBoats, many)1
-- sgl.query(ListBoats, many)2
SELECT id, name, home_port FROM boats ORDER BY id;2
SELECT id, name, home_port FROM boats ORDER BY id;What sgl generates
AddBoatidentical
MySQL
TimescaleDB
1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error1
func (queries *Queries) AddBoat(ctx context.Context, params AddBoatParams) error2
2
3
type AddBoatParams struct {3
type AddBoatParams struct {4
Name string4
Name string5
HomePort string5
HomePort string6
}6
}ListBoatsidentical
MySQL
TimescaleDB
1
func (queries *Queries) ListBoats(ctx context.Context) ([]ListBoatsRow, error)1
func (queries *Queries) ListBoats(ctx context.Context) ([]ListBoatsRow, error)2
2
3
type ListBoatsRow struct {3
type ListBoatsRow struct {4
ID int64 `json:"id"`4
ID int64 `json:"id"`5
Name string `json:"name"`5
Name string `json:"name"`6
HomePort string `json:"home_port"`6
HomePort string `json:"home_port"`7
}7
}The program
example.godiffers
MySQL
TimescaleDB
1
package main1
package main2
2
3
import (3
import (4
"context"4
"context"5
"database/sql"5
"database/sql"6
"fmt"6
"fmt"7
"io"7
"io"8
"io/fs"8
"io/fs"9
9
10
"seagull.sh/seagull/examples/mysql/027_migrations_at_startup/internal/store"10
"seagull.sh/seagull/examples/postgres/027_migrations_at_startup/internal/store"11
"seagull.sh/seagull/sdk/migrate"11
"seagull.sh/seagull/sdk/migrate"12
"seagull.sh/seagull/sdk/migrate/migrate_backend_mysql"12
"seagull.sh/seagull/sdk/migrate/migrate_backend_postgres"13
)13
)14
14
15
// run starts the program twice against the same database. The first start applies both15
// run starts the program twice against the same database. The first start applies both16
// migrations before the program uses the schema; the second finds nothing to do.16
// migrations before the program uses the schema; the second finds nothing to do.17
//17
//18
// Takes database (*sql.DB) which is the database to migrate, empty or not.18
// Takes database (*sql.DB) which is the database to migrate, empty or not.19
// Takes migrations (fs.FS) which holds db/migrations, the embed.FS in main.go.19
// Takes migrations (fs.FS) which holds db/migrations, the embed.FS in main.go.20
// Takes output (io.Writer) which receives what each start did.20
// Takes output (io.Writer) which receives what each start did.21
//21
//22
// Returns error when migrating or a query fails.22
// Returns error when migrating or a query fails.23
func run(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {23
func run(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {24
for _, boat := range []string{"Kittiwake", "Puffin"} {24
for _, boat := range []string{"Kittiwake", "Puffin"} {25
if err := start(ctx, database, migrations, boat, output); err != nil {25
if err := start(ctx, database, migrations, boat, output); err != nil {26
return err26
return err27
}27
}28
}28
}29
return printHistory(ctx, database, migrations, output)29
return printHistory(ctx, database, migrations, output)30
}30
}31
31
32
// start is what the program does each time it starts: migrate first, then work. Here the32
// start is what the program does each time it starts: migrate first, then work. Here the33
// work is adding a boat and listing the harbour.33
// work is adding a boat and listing the harbour.34
//34
//35
// Takes database (*sql.DB) which is the database.35
// Takes database (*sql.DB) which is the database.36
// Takes migrations (fs.FS) which holds the migration files.36
// Takes migrations (fs.FS) which holds the migration files.37
// Takes boat (string) which names the boat to add.37
// Takes boat (string) which names the boat to add.38
// Takes output (io.Writer) which receives what happened.38
// Takes output (io.Writer) which receives what happened.39
//39
//40
// Returns error when migrating or a query fails.40
// Returns error when migrating or a query fails.41
func start(ctx context.Context, database *sql.DB, migrations fs.FS, boat string, output io.Writer) error {41
func start(ctx context.Context, database *sql.DB, migrations fs.FS, boat string, output io.Writer) error {42
// migrate_backend_mysql.New() is the MySQL backend: it knows how to create the history42
// migrate_backend_postgres.New() is the PostgreSQL backend: it knows how to create the43
// table and holds a GET_LOCK advisory lock, scoped to this schema, while it works, so43
// history table and run each migration in a transaction, as PostgreSQL's DDL is44
// when several copies of a service start at once, one migrates and the rest wait, then44
// transactional. It also holds an advisory lock while it works, so when several copies45
// find nothing to do. MySQL commits each CREATE or ALTER as it runs, so a migration45
// of a service start at once, one migrates and the rest wait, then find nothing to do.46
// cannot be rolled back as a whole: keep each one to a single statement of DDL.47
// NewMigrator reads the numbered *.up.sql and *.down.sql files from the directory inside46
// NewMigrator reads the numbered *.up.sql and *.down.sql files from the directory inside48
// migrations.47
// migrations.49
migrator, err := migrate.NewMigrator(database, migrate_backend_mysql.New(), migrations, "db/migrations")48
migrator, err := migrate.NewMigrator(database, migrate_backend_postgres.New(), migrations, "db/migrations")50
if err != nil {49
if err != nil {51
return err50
return err52
}51
}53
52
54
// Up applies every migration not yet in the history, in version order, and returns how53
// Up applies every migration not yet in the history, in version order, and returns how55
// many it applied. On a database that is up to date it applies none.54
// many it applied. On a database that is up to date it applies none.56
applied, err := migrator.Up(ctx)55
applied, err := migrator.Up(ctx)57
if err != nil {56
if err != nil {58
return fmt.Errorf("migrating: %w", err)57
return fmt.Errorf("migrating: %w", err)59
}58
}60
fmt.Fprintf(output, "start-up: applied %d migrations\n", applied)59
fmt.Fprintf(output, "start-up: applied %d migrations\n", applied)61
60
62
queries := store.New(database)61
queries := store.New(database)63
if err := queries.AddBoat(ctx, store.AddBoatParams{Name: boat, HomePort: "Padstow"}); err != nil {62
if err := queries.AddBoat(ctx, store.AddBoatParams{Name: boat, HomePort: "Padstow"}); err != nil {64
return fmt.Errorf("adding %s: %w", boat, err)63
return fmt.Errorf("adding %s: %w", boat, err)65
}64
}66
boats, err := queries.ListBoats(ctx)65
boats, err := queries.ListBoats(ctx)67
if err != nil {66
if err != nil {68
return fmt.Errorf("listing boats: %w", err)67
return fmt.Errorf("listing boats: %w", err)69
}68
}70
for _, row := range boats {69
for _, row := range boats {71
fmt.Fprintf(output, " %d %-9s of %s\n", row.ID, row.Name, row.HomePort)70
fmt.Fprintf(output, " %d %-9s of %s\n", row.ID, row.Name, row.HomePort)72
}71
}73
return nil72
return nil74
}73
}75
74
76
// printHistory prints the migration history the migrator keeps in the database.75
// printHistory prints the migration history the migrator keeps in the database.77
//76
//78
// Takes database (*sql.DB) which is the migrated database.77
// Takes database (*sql.DB) which is the migrated database.79
// Takes migrations (fs.FS) which holds the migration files.78
// Takes migrations (fs.FS) which holds the migration files.80
// Takes output (io.Writer) which receives the history.79
// Takes output (io.Writer) which receives the history.81
//80
//82
// Returns error when the history cannot be read.81
// Returns error when the history cannot be read.83
func printHistory(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {82
func printHistory(ctx context.Context, database *sql.DB, migrations fs.FS, output io.Writer) error {84
backend := migrate_backend_mysql.New()83
backend := migrate_backend_postgres.New()85
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")84
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")86
if err != nil {85
if err != nil {87
return err86
return err88
}87
}89
statuses, err := migrator.Status(ctx)88
statuses, err := migrator.Status(ctx)90
if err != nil {89
if err != nil {91
return fmt.Errorf("reading migration status: %w", err)90
return fmt.Errorf("reading migration status: %w", err)92
}91
}93
fmt.Fprintf(output, "history in %s:\n", backend.Names().History)92
fmt.Fprintf(output, "history in %s:\n", backend.Names().History)94
for _, status := range statuses {93
for _, status := range statuses {95
fmt.Fprintf(output, " %04d %-15s applied=%t\n", status.Version, status.Name, status.Applied)94
fmt.Fprintf(output, " %04d %-15s applied=%t\n", status.Version, status.Name, status.Applied)96
}95
}97
return nil96
return nil98
}97
}