-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(migrations): Adding migrations to be up and downable (#24)
* adding create for the migrations * Adding migration files * Updating the examples
- Loading branch information
1 parent
af511ee
commit 90d0c93
Showing
57 changed files
with
12,451 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/Jacobbrewer1/goschema/pkg/migrations" | ||
"github.com/google/subcommands" | ||
) | ||
|
||
type createCmd struct { | ||
// name is the name of the migration to create. | ||
name string | ||
|
||
// OutputLocation is the location to write the generated files to. | ||
outputLocation string | ||
} | ||
|
||
func (c *createCmd) Name() string { | ||
return "create" | ||
} | ||
|
||
func (c *createCmd) Synopsis() string { | ||
return "Create a new migration" | ||
} | ||
|
||
func (c *createCmd) Usage() string { | ||
return `create: | ||
Create a new migration. | ||
` | ||
} | ||
|
||
func (c *createCmd) SetFlags(f *flag.FlagSet) { | ||
f.StringVar(&c.name, "name", "", "The name of the migration to create.") | ||
f.StringVar(&c.outputLocation, "out", ".", "The location to write the generated files to.") | ||
} | ||
|
||
func (c *createCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { | ||
if c.name == "" { | ||
slog.Error("Name is required") | ||
return subcommands.ExitUsageError | ||
} | ||
|
||
if c.outputLocation == "" { | ||
slog.Error("Output location is required") | ||
return subcommands.ExitUsageError | ||
} | ||
|
||
// File name is timestamp_name.up.sql and timestamp_name.down.sql | ||
// The timestamp is the current time in the format YYYYMMDDHHMMSS | ||
// The name is the name of the migration with spaces as underscores | ||
|
||
now := time.Now() | ||
name := fmt.Sprintf("%s_%s", now.Format(migrations.FilePrefix), strings.TrimSpace(c.name)) | ||
name = strings.ReplaceAll(name, " ", "_") | ||
|
||
upName := fmt.Sprintf("%s.up.sql", name) | ||
downName := fmt.Sprintf("%s.down.sql", name) | ||
|
||
upPath := fmt.Sprintf("%s/%s", c.outputLocation, upName) | ||
downPath := fmt.Sprintf("%s/%s", c.outputLocation, downName) | ||
|
||
upAbs, err := filepath.Abs(upPath) | ||
if err != nil { | ||
slog.Error("Error getting absolute path", slog.String("path", upPath), slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
downAbs, err := filepath.Abs(downPath) | ||
if err != nil { | ||
slog.Error("Error getting absolute path", slog.String("path", downPath), slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
if err := createFile(upAbs); err != nil { | ||
slog.Error("Error creating file", slog.String("path", upAbs), slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
slog.Info("Up migration created", slog.String("path", upAbs)) | ||
|
||
if err := createFile(downAbs); err != nil { | ||
slog.Error("Error creating file", slog.String("path", downAbs), slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
slog.Info("Down migration created", slog.String("path", downAbs)) | ||
|
||
return subcommands.ExitSuccess | ||
} | ||
|
||
func createFile(name string) error { | ||
// Create the path if it does not exist. | ||
dir := filepath.Dir(name) | ||
if err := os.MkdirAll(dir, os.ModePerm); err != nil { | ||
return fmt.Errorf("error creating path: %w", err) | ||
} | ||
|
||
f, err := os.Create(name) | ||
if err != nil { | ||
return fmt.Errorf("error creating file: %w", err) | ||
} | ||
|
||
return f.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"log/slog" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/Jacobbrewer1/goschema/pkg/migrations" | ||
"github.com/google/subcommands" | ||
) | ||
|
||
type migrateCmd struct { | ||
// up is the flag to migrate up. | ||
up bool | ||
|
||
// down is the flag to migrate down. | ||
down bool | ||
|
||
// migrationLocation is where the migrations are located. | ||
migrationLocation string | ||
} | ||
|
||
func (m *migrateCmd) Name() string { | ||
return "migrate" | ||
} | ||
|
||
func (m *migrateCmd) Synopsis() string { | ||
return "Migrate the database" | ||
} | ||
|
||
func (m *migrateCmd) Usage() string { | ||
return `migrate: | ||
Migrate the database. | ||
` | ||
} | ||
|
||
func (m *migrateCmd) SetFlags(f *flag.FlagSet) { | ||
f.BoolVar(&m.up, "up", false, "Migrate up.") | ||
f.BoolVar(&m.down, "down", false, "Migrate down.") | ||
f.StringVar(&m.migrationLocation, "loc", "./migrations", "The location of the migrations.") | ||
} | ||
|
||
func (m *migrateCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { | ||
if m.up && m.down { | ||
slog.Error("Cannot migrate up and down at the same time") | ||
return subcommands.ExitUsageError | ||
} else if !m.up && !m.down { | ||
slog.Error("Must specify up or down") | ||
return subcommands.ExitUsageError | ||
} | ||
|
||
if e := os.Getenv(migrations.DbEnvVar); e == "" { | ||
slog.Error(fmt.Sprintf("Environment variable %s is not set", migrations.DbEnvVar)) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
absPath, err := filepath.Abs(m.migrationLocation) | ||
if err != nil { | ||
slog.Error("Error getting absolute path", slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
db, err := migrations.ConnectDB() | ||
if err != nil { | ||
slog.Error("Error connecting to the database", slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
|
||
switch { | ||
case m.up: | ||
if err := migrations.NewVersioning(db, absPath).MigrateUp(); err != nil { | ||
slog.Error("Error migrating up", slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
case m.down: | ||
if err := migrations.NewVersioning(db, absPath).MigrateDown(); err != nil { | ||
slog.Error("Error migrating down", slog.String("error", err.Error())) | ||
return subcommands.ExitFailure | ||
} | ||
} | ||
|
||
slog.Info("Migration complete") | ||
|
||
return subcommands.ExitSuccess | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
#!/bin/bash | ||
|
||
function fail() { | ||
gum style --foreground 196 "$1" | ||
exit 1 | ||
} | ||
|
||
if ! command -v gum &> /dev/null; then | ||
echo "gum is required to generate models.'" | ||
echo "Trying to install gum..." | ||
go install github.com/charmbracelet/gum@latest || fail "Failed to install gum" | ||
fi | ||
|
||
if ! command -v goschema &> /dev/null; then | ||
gum style --foreground 196 "goschema is required to generate models. Please install it" | ||
exit 1 | ||
fi | ||
|
||
if ! command -v goimports &> /dev/null; then | ||
gum style --foreground 196 "goimports is required to generate models. Please install it by running 'go get golang.org/x/tools/cmd/goimports'" | ||
exit 1 | ||
fi | ||
|
||
up=false | ||
down=false | ||
forced=false | ||
|
||
# Get the flags passed to the script and set the variables accordingly | ||
while getopts "udcf" flag; do | ||
case $flag in | ||
u) | ||
up=true | ||
;; | ||
d) | ||
down=true | ||
;; | ||
f) | ||
forced=true | ||
;; | ||
*) | ||
gum style --foreground 196 "Invalid flag $flag" | ||
exit 1 | ||
;; | ||
esac | ||
done | ||
|
||
option="" | ||
|
||
if [ "$up" = false ] && [ "$down" = false ]; then | ||
# Assume being ran by the user | ||
gum style --foreground 222 "Please choose an option" | ||
option=$(gum choose "create" "up" "down") | ||
fi | ||
|
||
if [ "$up" = true ] && [ "$down" = true ]; then | ||
gum style --foreground 196 "Cannot run both up and down migrations" | ||
exit 1 | ||
fi | ||
|
||
case $option in | ||
up) | ||
# Is the DATABASE_URL set? | ||
if [ -z "$DATABASE_URL" ]; then | ||
gum style --foreground 196 "DATABASE_URL is not set" | ||
exit 1 | ||
fi | ||
|
||
gum spin --spinner dot --title "Running up migrations" -- goschema migrate --up --loc=./migrations | ||
;; | ||
down) | ||
# Is the DATABASE_URL set? | ||
if [ -z "$DATABASE_URL" ]; then | ||
gum style --foreground 196 "DATABASE_URL is not set" | ||
exit 1 | ||
fi | ||
|
||
gum spin --spinner dot --title "Running down migrations" -- goschema migrate --down --loc=./migrations | ||
;; | ||
create) | ||
name=$(gum input --placeholder "Please describe the migration") | ||
gum spin --spinner dot --title "Creating migrations" -- goschema create --out=./migrations --name="$name" | ||
esac |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DROP TABLE test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
CREATE TABLE test ( | ||
id SERIAL PRIMARY KEY, | ||
name TEXT NOT NULL | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package migrations | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"strings" | ||
|
||
_ "github.com/go-sql-driver/mysql" | ||
"github.com/jmoiron/sqlx" | ||
) | ||
|
||
const ( | ||
DbEnvVar = "DATABASE_URL" | ||
) | ||
|
||
func ConnectDB() (*sqlx.DB, error) { | ||
// Get the connection string. | ||
connStr := getConnectionStr() | ||
// Open the database connection. | ||
db, err := sqlx.Open("mysql", connStr) | ||
if err != nil { | ||
return nil, fmt.Errorf("error opening database: %w", err) | ||
} | ||
return db, nil | ||
} | ||
|
||
func getConnectionStr() string { | ||
// Get the connection string from the environment. | ||
connStr := os.Getenv(DbEnvVar) | ||
// Append "?timeout=90s&multiStatements=true&parseTime=true" to the connection string. But remove any current query string. | ||
if strings.Contains(connStr, "?") { | ||
connStr = strings.Split(connStr, "?")[0] | ||
} | ||
return fmt.Sprintf("%s?timeout=90s&multiStatements=true&parseTime=true", connStr) | ||
} |
Oops, something went wrong.