Migrations at start-up: TimescaleDB and PostgreSQL
TimescaleDB and PostgreSQL run the same files for this example: the SQL and the Go are identical.
Configuration
sgl.yamlidentical
TimescaleDB
PostgreSQL
1
version: 11
version: 12
2
3
databases:3
databases:4
- name: harbour4
- name: harbour5
dialect: postgres5
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/sql11
driver: database/sql12
migrate:12
migrate:13
dsn: ${env:DATABASE_URL}13
dsn: ${env:DATABASE_URL}Migrations
db/migrations/0001_boats.up.sqlidentical
TimescaleDB
PostgreSQL
1
CREATE TABLE boats (1
CREATE TABLE boats (2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,2
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,3
name TEXT NOT NULL3
name TEXT NOT NULL4
);4
);db/migrations/0001_boats.down.sqlidentical
TimescaleDB
PostgreSQL
1
DROP TABLE boats;1
DROP TABLE boats;db/migrations/0002_boat_home_port.up.sqlidentical
TimescaleDB
PostgreSQL
1
ALTER TABLE boats ADD COLUMN home_port TEXT 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
TimescaleDB
PostgreSQL
1
ALTER TABLE boats DROP COLUMN home_port;1
ALTER TABLE boats DROP COLUMN home_port;Queries
AddBoatidentical
TimescaleDB · db/queries/boats.sql
PostgreSQL · db/queries/boats.sql
1
-- sgl.query(AddBoat, exec)1
-- sgl.query(AddBoat, exec)2
INSERT INTO boats (name, home_port) VALUES ($1, $2);2
INSERT INTO boats (name, home_port) VALUES ($1, $2);ListBoatsidentical
TimescaleDB · db/queries/boats.sql
PostgreSQL · 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
TimescaleDB
PostgreSQL
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
TimescaleDB
PostgreSQL
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.goidentical
TimescaleDB
PostgreSQL
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/postgres/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_postgres"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_postgres.New() is the PostgreSQL backend: it knows how to create the42
// migrate_backend_postgres.New() is the PostgreSQL backend: it knows how to create the43
// history table and run each migration in a transaction, as PostgreSQL's DDL is43
// history table and run each migration in a transaction, as PostgreSQL's DDL is44
// transactional. It also holds an advisory lock while it works, so when several copies44
// transactional. It also holds an advisory lock while it works, so when several copies45
// of a service start at once, one migrates and the rest wait, then find nothing to do.45
// of a service start at once, one migrates and the rest wait, then find nothing to do.46
// 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 inside47
// migrations.47
// migrations.48
migrator, err := migrate.NewMigrator(database, migrate_backend_postgres.New(), migrations, "db/migrations")48
migrator, err := migrate.NewMigrator(database, migrate_backend_postgres.New(), migrations, "db/migrations")49
if err != nil {49
if err != nil {50
return err50
return err51
}51
}52
52
53
// 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 how54
// 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.55
applied, err := migrator.Up(ctx)55
applied, err := migrator.Up(ctx)56
if err != nil {56
if err != nil {57
return fmt.Errorf("migrating: %w", err)57
return fmt.Errorf("migrating: %w", err)58
}58
}59
fmt.Fprintf(output, "start-up: applied %d migrations\n", applied)59
fmt.Fprintf(output, "start-up: applied %d migrations\n", applied)60
60
61
queries := store.New(database)61
queries := store.New(database)62
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 {63
return fmt.Errorf("adding %s: %w", boat, err)63
return fmt.Errorf("adding %s: %w", boat, err)64
}64
}65
boats, err := queries.ListBoats(ctx)65
boats, err := queries.ListBoats(ctx)66
if err != nil {66
if err != nil {67
return fmt.Errorf("listing boats: %w", err)67
return fmt.Errorf("listing boats: %w", err)68
}68
}69
for _, row := range boats {69
for _, row := range boats {70
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)71
}71
}72
return nil72
return nil73
}73
}74
74
75
// printHistory prints the migration history the migrator keeps in the database.75
// printHistory prints the migration history the migrator keeps in the database.76
//76
//77
// Takes database (*sql.DB) which is the migrated database.77
// Takes database (*sql.DB) which is the migrated database.78
// Takes migrations (fs.FS) which holds the migration files.78
// Takes migrations (fs.FS) which holds the migration files.79
// Takes output (io.Writer) which receives the history.79
// Takes output (io.Writer) which receives the history.80
//80
//81
// Returns error when the history cannot be read.81
// Returns error when the history cannot be read.82
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 {83
backend := migrate_backend_postgres.New()83
backend := migrate_backend_postgres.New()84
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")84
migrator, err := migrate.NewMigrator(database, backend, migrations, "db/migrations")85
if err != nil {85
if err != nil {86
return err86
return err87
}87
}88
statuses, err := migrator.Status(ctx)88
statuses, err := migrator.Status(ctx)89
if err != nil {89
if err != nil {90
return fmt.Errorf("reading migration status: %w", err)90
return fmt.Errorf("reading migration status: %w", err)91
}91
}92
fmt.Fprintf(output, "history in %s:\n", backend.Names().History)92
fmt.Fprintf(output, "history in %s:\n", backend.Names().History)93
for _, status := range statuses {93
for _, status := range statuses {94
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)95
}95
}96
return nil96
return nil97
}97
}