diff --git a/docker-compose.yml b/docker-compose.yml index b1591c7..2b13d8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ go-test: - postgres - mysql - cassandra + - crate go-build: <<: *go command: sh -c 'go get -v && go build -ldflags ''-s'' -o migrater' @@ -24,3 +25,5 @@ mysql: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" cassandra: image: cassandra:2.2 +crate: + image: crate diff --git a/driver/crate/README.md b/driver/crate/README.md new file mode 100644 index 0000000..a43d013 --- /dev/null +++ b/driver/crate/README.md @@ -0,0 +1,15 @@ +# Crate driver + +This is a driver for the [Crate](https://crate.io) database. It is based on the Crate +sql driver by [herenow](https://github.com/herenow/go-crate). + +This driver does not use transactions! This is not a limitation of the driver, but a +limitation of Crate. So handle situations with failed migrations with care! + +## Usage + +```bash +migrate -url http://host:port -path ./db/migrations create add_field_to_table +migrate -url http://host:port -path ./db/migrations up +migrate help # for more info +``` \ No newline at end of file diff --git a/driver/crate/crate.go b/driver/crate/crate.go new file mode 100644 index 0000000..eb30853 --- /dev/null +++ b/driver/crate/crate.go @@ -0,0 +1,115 @@ +// Package crate implements a driver for the Crate.io database +package crate + +import ( + "database/sql" + "fmt" + "strings" + + _ "github.com/herenow/go-crate" + "github.com/mattes/migrate/driver" + "github.com/mattes/migrate/file" + "github.com/mattes/migrate/migrate/direction" +) + +func init() { + driver.RegisterDriver("crate", &Driver{}) +} + +type Driver struct { + db *sql.DB +} + +const tableName = "schema_migrations" + +func (driver *Driver) Initialize(url string) error { + url = strings.Replace(url, "crate", "http", 1) + db, err := sql.Open("crate", url) + if err != nil { + return err + } + if err := db.Ping(); err != nil { + return err + } + driver.db = db + + if err := driver.ensureVersionTableExists(); err != nil { + return err + } + return nil +} + +func (driver *Driver) Close() error { + if err := driver.db.Close(); err != nil { + return err + } + return nil +} + +func (driver *Driver) FilenameExtension() string { + return "sql" +} + +func (driver *Driver) Version() (uint64, error) { + var version uint64 + err := driver.db.QueryRow("SELECT version FROM " + tableName + " ORDER BY version DESC LIMIT 1").Scan(&version) + switch { + case err == sql.ErrNoRows: + return 0, nil + case err != nil: + return 0, err + default: + return version, nil + } +} + +func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { + defer close(pipe) + pipe <- f + + if err := f.ReadContent(); err != nil { + pipe <- err + return + } + + lines := splitContent(string(f.Content)) + for _, line := range lines { + _, err := driver.db.Exec(line) + if err != nil { + pipe <- err + return + } + } + + if f.Direction == direction.Up { + if _, err := driver.db.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil { + pipe <- err + return + } + } else if f.Direction == direction.Down { + if _, err := driver.db.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil { + pipe <- err + return + } + } +} + +func splitContent(content string) []string { + lines := strings.Split(content, ";") + resultLines := make([]string, 0, len(lines)) + for i, line := range lines { + line = strings.Replace(lines[i], ";", "", -1) + line = strings.TrimSpace(line) + if line != "" { + resultLines = append(resultLines, line) + } + } + return resultLines +} + +func (driver *Driver) ensureVersionTableExists() error { + if _, err := driver.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (version INTEGER PRIMARY KEY)", tableName)); err != nil { + return err + } + return nil +} diff --git a/driver/crate/crate_test.go b/driver/crate/crate_test.go new file mode 100644 index 0000000..164cd57 --- /dev/null +++ b/driver/crate/crate_test.go @@ -0,0 +1,111 @@ +package crate + +import ( + "fmt" + "os" + "testing" + + "github.com/mattes/migrate/file" + "github.com/mattes/migrate/migrate/direction" + pipep "github.com/mattes/migrate/pipe" +) + +func TestContentSplit(t *testing.T) { + content := `CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0); +CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0); +CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0); + ` + + lines := splitContent(content) + if len(lines) != 3 { + t.Errorf("Expected 3 lines, but got %d", len(lines)) + } + + if lines[0] != "CREATE TABLE users (user_id STRING primary key, first_name STRING, last_name STRING, email STRING, password_hash STRING) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" { + t.Error("Line does not match expected output") + } + + if lines[1] != "CREATE TABLE units (unit_id STRING primary key, name STRING, members array(string)) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" { + t.Error("Line does not match expected output") + } + + if lines[2] != "CREATE TABLE available_connectors (technology_id STRING primary key, description STRING, icon STRING, link STRING, configuration_parameters array(object as (name STRING, type STRING))) CLUSTERED INTO 3 shards WITH (number_of_replicas = 0)" { + t.Error("Line does not match expected output") + } +} + +func TestMigrate(t *testing.T) { + host := os.Getenv("CRATE_PORT_4200_TCP_ADDR") + port := os.Getenv("CRATE_PORT_4200_TCP_PORT") + + url := fmt.Sprintf("crate://%s:%s", host, port) + + driver := &Driver{} + + if err := driver.Initialize(url); err != nil { + t.Fatal(err) + } + + successFiles := []file.File{ + { + Path: "/foobar", + FileName: "001_foobar.up.sql", + Version: 1, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + CREATE TABLE yolo ( + id integer primary key, + msg string + ); + `), + }, + { + Path: "/foobar", + FileName: "002_foobar.down.sql", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + DROP TABLE yolo; + `), + }, + } + + failFiles := []file.File{ + { + Path: "/foobar", + FileName: "002_foobar.up.sql", + Version: 2, + Name: "foobar", + Direction: direction.Up, + Content: []byte(` + CREATE TABLE error ( + id THIS WILL CAUSE AN ERROR + ) + `), + }, + } + + for _, file := range successFiles { + pipe := pipep.New() + go driver.Migrate(file, pipe) + errs := pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + } + + for _, file := range failFiles { + pipe := pipep.New() + go driver.Migrate(file, pipe) + errs := pipep.ReadErrors(pipe) + if len(errs) == 0 { + t.Fatal("Migration should have failed but succeeded") + } + } + + if err := driver.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/main.go b/main.go index 10925b1..d50671f 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/fatih/color" _ "github.com/mattes/migrate/driver/bash" _ "github.com/mattes/migrate/driver/cassandra" + _ "github.com/mattes/migrate/driver/crate" _ "github.com/mattes/migrate/driver/mysql" _ "github.com/mattes/migrate/driver/postgres" _ "github.com/mattes/migrate/driver/sqlite3"