From 66fb03fa0dbb5838ceebcd2ab8b1b56b401b23e1 Mon Sep 17 00:00:00 2001 From: Fluffy-Bean Date: Mon, 6 May 2024 17:24:24 +0100 Subject: [PATCH] Add migration, status and pending checks to database --- cmd/cmd.go | 2 + cmd/database.go | 49 ++++---------- cmd/run.go | 16 +++++ database/database.go | 12 ++-- .../migrations/20240506163617-init_tables.sql | 11 ++++ database/migrator.go | 64 +++++++++++++++++++ dbconfig.yml | 9 +++ go.mod | 4 +- go.sum | 21 +++++- 9 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 database/migrations/20240506163617-init_tables.sql create mode 100644 database/migrator.go create mode 100644 dbconfig.yml diff --git a/cmd/cmd.go b/cmd/cmd.go index 8ca851c..2500565 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -16,6 +16,8 @@ func Parse() { run(os.Args[2:]) case "migrate": migrate(os.Args[2:]) + case "status": + status(os.Args[2:]) case "-h": case "--help": fmt.Println("Available commands are:") diff --git a/cmd/database.go b/cmd/database.go index 0b6c2c2..83ea9c0 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -3,19 +3,15 @@ package cmd import ( "flag" "fmt" - "log" "os" db "github.com/Fluffy-Bean/TastyBites/database" - sb "github.com/huandu/go-sqlbuilder" ) func migrate(flags []string) { - cmd := flag.NewFlagSet("migrate", flag.ExitOnError) - tables := cmd.Bool("tables", false, "Create tables") - verbose := cmd.Bool("v", false, "Verbose") + downgrade := cmd.Bool("downgrade", false, "Downgrade Database") err := cmd.Parse(flags) if err != nil { @@ -23,36 +19,17 @@ func migrate(flags []string) { os.Exit(1) } - if !*tables { - builder := db.ItemStruct.InsertInto("Item") - for _, item := range db.SampleData { - builder.Values(item.UUID, item.Name, item.Price, item.Description) - } - query, args := builder.Build() - - _, err = db.Conn.Exec(query, args...) - if err != nil { - log.Fatal(err) - } else { - fmt.Println("Success") - } - } else { - itemTable := sb.CreateTable("Item").IfNotExists() - itemTable.Define("uuid", "TEXT", "NOT NULL", "PRIMARY KEY") - itemTable.Define("name", "TEXT", "NOT NULL") - itemTable.Define("price", "INTEGER", "NOT NULL") - itemTable.Define("description", "TEXT") - query, args := itemTable.BuildWithFlavor(sb.SQLite) - - if *verbose { - fmt.Println(query, args) - } - - _, err := db.Conn.Exec(query, args...) - if err != nil { - log.Fatal(err) - } else { - fmt.Println("Success") - } + err = db.Migrate(!*downgrade) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func status(flags []string) { + err := db.Status() + if err != nil { + fmt.Println(err) + os.Exit(1) } } diff --git a/cmd/run.go b/cmd/run.go index 3d9c102..ec009a6 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -6,12 +6,14 @@ import ( "os" "github.com/Fluffy-Bean/TastyBites/api" + db "github.com/Fluffy-Bean/TastyBites/database" ) func run(flags []string) { cmd := flag.NewFlagSet("run", flag.ExitOnError) host := cmd.String("host", "127.0.0.1:8080", "Host address, such as 0.0.0.0:8080") + skip := cmd.Bool("skipMigrations", false, "Skip any pending migrations") logging := cmd.Bool("log", false, "Enable logging for the application") err := cmd.Parse(flags) @@ -20,6 +22,20 @@ func run(flags []string) { os.Exit(1) } + if !*skip { + pending, err := db.Pending() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if pending { + fmt.Println("Pending migrations!") + fmt.Println("Run `TastyBites status` or use the flag -skipMigrations") + os.Exit(1) + } + } + api.Serve(api.Config{ Host: *host, Logging: *logging, diff --git a/database/database.go b/database/database.go index 9b73d5f..dffeed0 100644 --- a/database/database.go +++ b/database/database.go @@ -2,7 +2,8 @@ package database import ( "database/sql" - "log" + "fmt" + "os" "github.com/mattn/go-sqlite3" ) @@ -16,19 +17,22 @@ func Open() { Conn, err = sql.Open("sqlite3", "tastybites.db?_journal_mode=WAL") if err != nil { - log.Fatal("Error opening connection: ", err) + fmt.Println("Error opening connection:", err) + os.Exit(1) } //Set the connection to use WAL mode _, err = Conn.Exec("PRAGMA journal_mode=WAL;") if err != nil { - log.Fatal(err) + fmt.Println(err) + os.Exit(1) } } func Close() { err := Conn.Close() if err != nil { - log.Fatal(err) + fmt.Println(err) + os.Exit(1) } } diff --git a/database/migrations/20240506163617-init_tables.sql b/database/migrations/20240506163617-init_tables.sql new file mode 100644 index 0000000..35b5d7b --- /dev/null +++ b/database/migrations/20240506163617-init_tables.sql @@ -0,0 +1,11 @@ + +-- +migrate Up +CREATE TABLE IF NOT EXISTS Item ( + uuid TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + price INTEGER NOT NULL, + description TEXT +); + +-- +migrate Down +DROP TABLE Item; diff --git a/database/migrator.go b/database/migrator.go new file mode 100644 index 0000000..a9de5f1 --- /dev/null +++ b/database/migrator.go @@ -0,0 +1,64 @@ +package database + +import ( + "embed" + "fmt" + + migrate "github.com/rubenv/sql-migrate" +) + +//go:embed migrations/* +var dbMigrations embed.FS +var migrations = migrate.EmbedFileSystemMigrationSource{ + FileSystem: dbMigrations, + Root: "migrations", +} + +func Migrate(Up bool) error { + var direction migrate.MigrationDirection + if Up { + direction = migrate.Up + } else { + direction = migrate.Down + } + + n, err := migrate.Exec(Conn, "sqlite3", migrations, direction) + if err != nil { + return err + } + + fmt.Printf("Applied %d migrations!\n", n) + return nil +} + +func Status() error { + // Get the list of applied migrations + applied, err := migrate.GetMigrationRecords(Conn, "sqlite3") + if err != nil { + return err + } + + // Get the list of migrations that are yet to be applied + planned, _, err := migrate.PlanMigration(Conn, "sqlite3", migrations, migrate.Up, 0) + if err != nil { + return err + } + + fmt.Println("Migration list:") + for _, migration := range applied { + fmt.Printf("DONE: %s\n", migration.Id) + } + for _, migration := range planned { + fmt.Printf("TODO: %s\n", migration.Id) + } + + return nil +} + +func Pending() (bool, error) { + planned, _, err := migrate.PlanMigration(Conn, "sqlite3", migrations, migrate.Up, 0) + if err != nil { + return false, err + } + return len(planned) > 0, nil +} diff --git a/dbconfig.yml b/dbconfig.yml new file mode 100644 index 0000000..58867db --- /dev/null +++ b/dbconfig.yml @@ -0,0 +1,9 @@ +development: + dialect: sqlite3 + datasource: tastybites.db + dir: database/migrations + +production: + dialect: sqlite3 + datasource: tastybites.db + dir: database/migrations diff --git a/go.mod b/go.mod index 0e92288..c2c5e30 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,13 @@ require ( github.com/huandu/go-sqlbuilder v1.27.1 github.com/labstack/echo/v4 v4.12.0 github.com/mattn/go-sqlite3 v1.14.22 + github.com/rubenv/sql-migrate v1.6.1 ) require ( + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/huandu/xstrings v1.3.2 // indirect + github.com/huandu/xstrings v1.4.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 8fe7f60..c6672cf 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,29 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= github.com/huandu/go-sqlbuilder v1.27.1 h1:7UU/3EMIQYYX8wn+L7BNcGVz1aEs5TPNOVFd7ryrPos= github.com/huandu/go-sqlbuilder v1.27.1/go.mod h1:nUVmMitjOmn/zacMLXT0d3Yd3RHoO2K+vy906JzqxMI= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -22,6 +33,12 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos= +github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -43,6 +60,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=