From 8d39928220ab1352ae5b67b7a9f9605dfdacb20f Mon Sep 17 00:00:00 2001 From: Caesar Wirth Date: Sun, 8 Mar 2015 11:10:35 +0900 Subject: [PATCH] Add driver for sqlite3 --- driver/driver.go | 8 +++ driver/sqlite3/README.md | 21 ++++++ driver/sqlite3/sqlite3.go | 125 +++++++++++++++++++++++++++++++++ driver/sqlite3/sqlite3_test.go | 94 +++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 driver/sqlite3/README.md create mode 100644 driver/sqlite3/sqlite3.go create mode 100644 driver/sqlite3/sqlite3_test.go diff --git a/driver/driver.go b/driver/driver.go index e73e5e7..9c00074 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -10,6 +10,7 @@ import ( "github.com/mattes/migrate/driver/cassandra" "github.com/mattes/migrate/driver/mysql" "github.com/mattes/migrate/driver/postgres" + "github.com/mattes/migrate/driver/sqlite3" "github.com/mattes/migrate/file" ) @@ -78,6 +79,13 @@ func New(url string) (Driver, error) { return nil, err } return d, nil + case "sqlite3": + d := &sqlite3.Driver{} + verifyFilenameExtension("sqlite3", d) + if err := d.Initialize(url); err != nil { + return nil, err + } + return d, nil default: return nil, errors.New(fmt.Sprintf("Driver '%s' not found.", u.Scheme)) } diff --git a/driver/sqlite3/README.md b/driver/sqlite3/README.md new file mode 100644 index 0000000..80cd361 --- /dev/null +++ b/driver/sqlite3/README.md @@ -0,0 +1,21 @@ +# Sqlite3 Driver + +* Runs migrations in transcations. + That means that if a migration failes, it will be safely rolled back. +* Tries to return helpful error messages. +* Stores migration version details in table ``schema_migrations``. + This table will be auto-generated. + + +## Usage + +```bash +migrate -url sqlite3://database.sqlite -path ./db/migrations create add_field_to_table +migrate -url sqlite3://database.sqlite -path ./db/migrations up +migrate help # for more info +``` + +## Authors + +* Matthias Kadenbach, https://github.com/mattes +* Caesar Wirth, https://github.com/cjwirth diff --git a/driver/sqlite3/sqlite3.go b/driver/sqlite3/sqlite3.go new file mode 100644 index 0000000..a33b7e5 --- /dev/null +++ b/driver/sqlite3/sqlite3.go @@ -0,0 +1,125 @@ +// Package sqlite3 implements the Driver interface. +package sqlite3 + +import ( + "database/sql" + "errors" + "fmt" + "github.com/mattes/migrate/file" + "github.com/mattes/migrate/migrate/direction" + "github.com/mattn/go-sqlite3" + "strings" +) + +type Driver struct { + db *sql.DB +} + +const tableName = "schema_migration" + +func (driver *Driver) Initialize(url string) error { + filename := strings.SplitN(url, "sqlite3://", 2) + if len(filename) != 2 { + return errors.New("invalid sqlite3:// scheme") + } + + db, err := sql.Open("sqlite3", filename[1]) + 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) ensureVersionTableExists() error { + if _, err := driver.db.Exec("CREATE TABLE IF NOT EXISTS " + tableName + " (version INTEGER PRIMARY KEY AUTOINCREMENT);"); err != nil { + return err + } + return nil +} + +func (driver *Driver) FilenameExtension() string { + return "sql" +} + +func (driver *Driver) Migrate(f file.File, pipe chan interface{}) { + defer close(pipe) + pipe <- f + + tx, err := driver.db.Begin() + if err != nil { + pipe <- err + return + } + + if f.Direction == direction.Up { + if _, err := tx.Exec("INSERT INTO "+tableName+" (version) VALUES (?)", f.Version); err != nil { + pipe <- err + if err := tx.Rollback(); err != nil { + pipe <- err + } + return + } + } else if f.Direction == direction.Down { + if _, err := tx.Exec("DELETE FROM "+tableName+" WHERE version=?", f.Version); err != nil { + pipe <- err + if err := tx.Rollback(); err != nil { + pipe <- err + } + return + } + } + + if err := f.ReadContent(); err != nil { + pipe <- err + return + } + + if _, err := tx.Exec(string(f.Content)); err != nil { + sqliteErr, isErr := err.(sqlite3.Error) + + if isErr { + // The sqlite3 library only provides error codes, not position information. Output what we do know + pipe <- errors.New(fmt.Sprintf("SQLite Error (%s); Extended (%s)\nError: %s", sqliteErr.Code.Error(), sqliteErr.ExtendedCode.Error(), sqliteErr.Error())) + } else { + pipe <- errors.New(fmt.Sprintf("An error occurred: %s", err.Error())) + } + + if err := tx.Rollback(); err != nil { + pipe <- err + } + return + } + + if err := tx.Commit(); err != nil { + pipe <- err + return + } +} + +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 + } +} diff --git a/driver/sqlite3/sqlite3_test.go b/driver/sqlite3/sqlite3_test.go new file mode 100644 index 0000000..270a25d --- /dev/null +++ b/driver/sqlite3/sqlite3_test.go @@ -0,0 +1,94 @@ +package sqlite3 + +import ( + "database/sql" + "github.com/mattes/migrate/file" + "github.com/mattes/migrate/migrate/direction" + pipep "github.com/mattes/migrate/pipe" + "testing" +) + +// TestMigrate runs some additional tests on Migrate() +// Basic testing is already done in migrate/migrate_test.go +func TestMigrate(t *testing.T) { + driverFile := ":memory:" + driverUrl := "sqlite3://" + driverFile + + // prepare clean database + connection, err := sql.Open("sqlite3", driverFile) + if err != nil { + t.Fatal(err) + } + if _, err := connection.Exec(` + DROP TABLE IF EXISTS yolo; + DROP TABLE IF EXISTS ` + tableName + `;`); err != nil { + t.Fatal(err) + } + + d := &Driver{} + if err := d.Initialize(driverUrl); err != nil { + t.Fatal(err) + } + + files := []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 AUTOINCREMENT + ); + `), + }, + { + Path: "/foobar", + FileName: "002_foobar.down.sql", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + DROP TABLE yolo; + `), + }, + { + Path: "/foobar", + FileName: "002_foobar.up.sql", + Version: 1, + Name: "foobar", + Direction: direction.Down, + Content: []byte(` + CREATE TABLE error ( + THIS; WILL CAUSE; AN ERROR; + ) + `), + }, + } + + pipe := pipep.New() + go d.Migrate(files[0], pipe) + errs := pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + + pipe = pipep.New() + go d.Migrate(files[1], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) > 0 { + t.Fatal(errs) + } + + pipe = pipep.New() + go d.Migrate(files[2], pipe) + errs = pipep.ReadErrors(pipe) + if len(errs) == 0 { + t.Error("Expected test case to fail") + } + + if err := d.Close(); err != nil { + t.Fatal(err) + } +}