diff --git a/.gitignore b/.gitignore index 00b92cb33..acb4088f2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ cli/cli cli/migrate .coverage .godoc.pid -vendor/ \ No newline at end of file +vendor/ +.vscode/ diff --git a/Dockerfile b/Dockerfile index 13ca3f7ff..cebd38bd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /go/src/github.com/golang-migrate/migrate COPY . ./ ENV GO111MODULE=on -ENV DATABASES="postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb" +ENV DATABASES="postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb mssql" ENV SOURCES="file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab" RUN go build -a -o build/migrate.linux-386 -ldflags="-X main.Version=${VERSION}" -tags "$DATABASES $SOURCES" ./cmd/migrate diff --git a/Makefile b/Makefile index b1afc221b..82842d2b6 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab -DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb +DATABASE ?= postgres mysql redshift cassandra spanner cockroachdb clickhouse mongodb mssql VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-) TEST_FLAGS ?= REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)") diff --git a/README.md b/README.md index d6f089fd2..ad006b6b8 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Database drivers run migrations. [Add a new database?](database/driver.go) * [CockroachDB](database/cockroachdb) * [ClickHouse](database/clickhouse) * [Firebird](database/firebird) ([todo #49](https://github.com/golang-migrate/migrate/issues/49)) + * [Postgres](database/postgres) ### Database URLs diff --git a/database/mssql/README.md b/database/mssql/README.md new file mode 100644 index 000000000..060f03ae6 --- /dev/null +++ b/database/mssql/README.md @@ -0,0 +1,21 @@ +# Microsoft SQL Server + +`sqlserver://username:password@host/instance?param1=value¶m2=value` +`sqlserver://username:password@host:port?param1=value¶m2=value` + +| URL Query | WithInstance Config | Description | +|------------|---------------------|-------------| +| `x-migrations-table` | `MigrationsTable` | Name of the migrations table | +| `username` | | enter the SQL Server Authentication user id or the Windows Authentication user id in the DOMAIN\User format. On Windows, if user id is empty or missing Single-Sign-On is used. | +| `password` | | The user's password. | +| `host` | | The host to connect to. | +| `port` | | The port to connect to. | +| `instance` | | SQL Server instance name. | +| `database` | `DatabaseName` | The name of the database to connect to | +| `connection+timeout` | | in seconds (default is 0 for no timeout), set to 0 for no timeout. | +| `dial+timeout` | | in seconds (default is 15), set to 0 for no timeout. | +| `encrypt` | | `disable` - Data send between client and server is not encrypted. `false` - Data sent between client and server is not encrypted beyond the login packet (Default). `true` - Data sent between client and server is encrypted. | +| `app+name` || The application name (default is go-mssqldb). | + +See https://github.com/denisenkom/go-mssqldb for full parameter list. + diff --git a/database/mssql/examples/migrations/1085649617_create_users_table.down.sql b/database/mssql/examples/migrations/1085649617_create_users_table.down.sql new file mode 100644 index 000000000..c99ddcdc8 --- /dev/null +++ b/database/mssql/examples/migrations/1085649617_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; diff --git a/database/mssql/examples/migrations/1085649617_create_users_table.up.sql b/database/mssql/examples/migrations/1085649617_create_users_table.up.sql new file mode 100644 index 000000000..92897dcab --- /dev/null +++ b/database/mssql/examples/migrations/1085649617_create_users_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + user_id integer unique, + name varchar(40), + email varchar(40) +); diff --git a/database/mssql/examples/migrations/1185749658_add_city_to_users.down.sql b/database/mssql/examples/migrations/1185749658_add_city_to_users.down.sql new file mode 100644 index 000000000..940c60712 --- /dev/null +++ b/database/mssql/examples/migrations/1185749658_add_city_to_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE users DROP COLUMN IF EXISTS city; diff --git a/database/mssql/examples/migrations/1185749658_add_city_to_users.up.sql b/database/mssql/examples/migrations/1185749658_add_city_to_users.up.sql new file mode 100644 index 000000000..2add820be --- /dev/null +++ b/database/mssql/examples/migrations/1185749658_add_city_to_users.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD city varchar(100); + + diff --git a/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.down.sql b/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.down.sql new file mode 100644 index 000000000..3e87dd229 --- /dev/null +++ b/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS users_email_index; diff --git a/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.up.sql b/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.up.sql new file mode 100644 index 000000000..03a04639c --- /dev/null +++ b/database/mssql/examples/migrations/1285849751_add_index_on_user_emails.up.sql @@ -0,0 +1,3 @@ +CREATE UNIQUE INDEX users_email_index ON users (email); + +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. diff --git a/database/mssql/examples/migrations/1385949617_create_books_table.down.sql b/database/mssql/examples/migrations/1385949617_create_books_table.down.sql new file mode 100644 index 000000000..1a0b1a214 --- /dev/null +++ b/database/mssql/examples/migrations/1385949617_create_books_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS books; diff --git a/database/mssql/examples/migrations/1385949617_create_books_table.up.sql b/database/mssql/examples/migrations/1385949617_create_books_table.up.sql new file mode 100644 index 000000000..f1503b518 --- /dev/null +++ b/database/mssql/examples/migrations/1385949617_create_books_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE books ( + user_id integer, + name varchar(40), + author varchar(40) +); diff --git a/database/mssql/examples/migrations/1485949617_create_movies_table.down.sql b/database/mssql/examples/migrations/1485949617_create_movies_table.down.sql new file mode 100644 index 000000000..3a5187689 --- /dev/null +++ b/database/mssql/examples/migrations/1485949617_create_movies_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS movies; diff --git a/database/mssql/examples/migrations/1485949617_create_movies_table.up.sql b/database/mssql/examples/migrations/1485949617_create_movies_table.up.sql new file mode 100644 index 000000000..f0ef5943b --- /dev/null +++ b/database/mssql/examples/migrations/1485949617_create_movies_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE movies ( + user_id integer, + name varchar(40), + director varchar(40) +); diff --git a/database/mssql/examples/migrations/1585849751_just_a_comment.up.sql b/database/mssql/examples/migrations/1585849751_just_a_comment.up.sql new file mode 100644 index 000000000..9b6b57a61 --- /dev/null +++ b/database/mssql/examples/migrations/1585849751_just_a_comment.up.sql @@ -0,0 +1 @@ +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. diff --git a/database/mssql/examples/migrations/1685849751_another_comment.up.sql b/database/mssql/examples/migrations/1685849751_another_comment.up.sql new file mode 100644 index 000000000..9b6b57a61 --- /dev/null +++ b/database/mssql/examples/migrations/1685849751_another_comment.up.sql @@ -0,0 +1 @@ +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. diff --git a/database/mssql/examples/migrations/1785849751_another_comment.up.sql b/database/mssql/examples/migrations/1785849751_another_comment.up.sql new file mode 100644 index 000000000..9b6b57a61 --- /dev/null +++ b/database/mssql/examples/migrations/1785849751_another_comment.up.sql @@ -0,0 +1 @@ +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. diff --git a/database/mssql/examples/migrations/1885849751_another_comment.up.sql b/database/mssql/examples/migrations/1885849751_another_comment.up.sql new file mode 100644 index 000000000..9b6b57a61 --- /dev/null +++ b/database/mssql/examples/migrations/1885849751_another_comment.up.sql @@ -0,0 +1 @@ +-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere. diff --git a/database/mssql/mssql.go b/database/mssql/mssql.go new file mode 100644 index 000000000..a7ca8ce25 --- /dev/null +++ b/database/mssql/mssql.go @@ -0,0 +1,339 @@ +package mssql + +import ( + "context" + "database/sql" + "fmt" + "io" + "io/ioutil" + nurl "net/url" + + mssql "github.com/denisenkom/go-mssqldb" // mssql support + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database" + "github.com/hashicorp/go-multierror" +) + +func init() { + db := MSSQL{} + database.Register("mssql", &db) + database.Register("sqlserver", &db) +} + +// DefaultMigrationsTable is the name of the migrations table in the database +var DefaultMigrationsTable = "schema_migrations" + +var ( + ErrNilConfig = fmt.Errorf("no config") + ErrNoDatabaseName = fmt.Errorf("no database name") + ErrNoSchema = fmt.Errorf("no schema") + ErrDatabaseDirty = fmt.Errorf("database is dirty") +) + +var lockErrorMap = map[mssql.ReturnStatus]string{ + -1: "The lock request timed out.", + -2: "The lock request was canceled.", + -3: "The lock request was chosen as a deadlock victim.", + -999: "Parameter validation or other call error.", +} + +// Config for database +type Config struct { + MigrationsTable string + DatabaseName string + SchemaName string +} + +// MSSQL connection +type MSSQL struct { + // Locking and unlocking need to use the same connection + conn *sql.Conn + db *sql.DB + isLocked bool + + // Open and WithInstance need to garantuee that config is never nil + config *Config +} + +// WithInstance returns a database instance from an already created database connection +func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) { + if config == nil { + return nil, ErrNilConfig + } + + if err := instance.Ping(); err != nil { + return nil, err + } + + query := `SELECT DB_NAME()` + var databaseName string + if err := instance.QueryRow(query).Scan(&databaseName); err != nil { + return nil, &database.Error{OrigErr: err, Query: []byte(query)} + } + + if len(databaseName) == 0 { + return nil, ErrNoDatabaseName + } + + config.DatabaseName = databaseName + + query = `SELECT SCHEMA_NAME()` + var schemaName string + if err := instance.QueryRow(query).Scan(&schemaName); err != nil { + return nil, &database.Error{OrigErr: err, Query: []byte(query)} + } + + if len(schemaName) == 0 { + return nil, ErrNoSchema + } + + config.SchemaName = schemaName + + if len(config.MigrationsTable) == 0 { + config.MigrationsTable = DefaultMigrationsTable + } + + conn, err := instance.Conn(context.Background()) + + if err != nil { + return nil, err + } + + ss := &MSSQL{ + conn: conn, + db: instance, + config: config, + } + + if err := ss.ensureVersionTable(); err != nil { + return nil, err + } + + return ss, nil +} + +// Open a connection to the database +func (ss *MSSQL) Open(url string) (database.Driver, error) { + purl, err := nurl.Parse(url) + if err != nil { + return nil, err + } + + db, err := sql.Open("mssql", migrate.FilterCustomQuery(purl).String()) + if err != nil { + return nil, err + } + + migrationsTable := purl.Query().Get("x-migrations-table") + + px, err := WithInstance(db, &Config{ + DatabaseName: purl.Path, + MigrationsTable: migrationsTable, + }) + + if err != nil { + return nil, err + } + + return px, nil +} + +// Close the database connection +func (ss *MSSQL) Close() error { + connErr := ss.conn.Close() + dbErr := ss.db.Close() + if connErr != nil || dbErr != nil { + return fmt.Errorf("conn: %v, db: %v", connErr, dbErr) + } + return nil +} + +// Lock creates an advisory local on the database to prevent multiple migrations from running at the same time. +func (ss *MSSQL) Lock() error { + if ss.isLocked { + return database.ErrLocked + } + + aid, err := database.GenerateAdvisoryLockId(ss.config.DatabaseName, ss.config.SchemaName) + if err != nil { + return err + } + + // This will either obtain the lock immediately and return true, + // or return false if the lock cannot be acquired immediately. + // MS Docs: sp_getapplock: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql?view=sql-server-2017 + query := `EXEC sp_getapplock @Resource = ?, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 0` + + var status mssql.ReturnStatus + if _, err = ss.conn.ExecContext(context.Background(), query, aid, &status); err == nil && status > -1 { + ss.isLocked = true + return nil + } else if err != nil { + return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)} + } else { + return &database.Error{Err: fmt.Sprintf("try lock failed with error %v: %v", status, lockErrorMap[status]), Query: []byte(query)} + } +} + +// Unlock froms the migration lock from the database +func (ss *MSSQL) Unlock() error { + if !ss.isLocked { + return nil + } + + aid, err := database.GenerateAdvisoryLockId(ss.config.DatabaseName, ss.config.SchemaName) + if err != nil { + return err + } + + // MS Docs: sp_releaseapplock: https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-releaseapplock-transact-sql?view=sql-server-2017 + query := `EXEC sp_releaseapplock @Resource = ?, @LockOwner = 'Session'` + if _, err := ss.conn.ExecContext(context.Background(), query, aid); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + ss.isLocked = false + + return nil +} + +// Run the migrations for the database +func (ss *MSSQL) Run(migration io.Reader) error { + migr, err := ioutil.ReadAll(migration) + if err != nil { + return err + } + + // run migration + query := string(migr[:]) + if _, err := ss.conn.ExecContext(context.Background(), query); err != nil { + if msErr, ok := err.(mssql.Error); ok { + message := fmt.Sprintf("migration failed: %s", msErr.Message) + if msErr.ProcName != "" { + message = fmt.Sprintf("%s (proc name %s)", msErr.Message, msErr.ProcName) + } + return database.Error{OrigErr: err, Err: message, Query: migr, Line: uint(msErr.LineNo)} + } + return database.Error{OrigErr: err, Err: "migration failed", Query: migr} + } + + return nil +} + +// SetVersion for the current database +func (ss *MSSQL) SetVersion(version int, dirty bool) error { + + tx, err := ss.conn.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return &database.Error{OrigErr: err, Err: "transaction start failed"} + } + + query := `TRUNCATE TABLE "` + ss.config.MigrationsTable + `"` + if _, err := tx.Exec(query); err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = multierror.Append(err, errRollback) + } + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + if version >= 0 { + var dirtyBit int + if dirty { + dirtyBit = 1 + } + query = `INSERT INTO "` + ss.config.MigrationsTable + `" (version, dirty) VALUES ($1, $2)` + if _, err := tx.Exec(query, version, dirtyBit); err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + err = multierror.Append(err, errRollback) + } + return &database.Error{OrigErr: err, Query: []byte(query)} + } + } + + if err := tx.Commit(); err != nil { + return &database.Error{OrigErr: err, Err: "transaction commit failed"} + } + + return nil +} + +// Version of the current database state +func (ss *MSSQL) Version() (version int, dirty bool, err error) { + query := `SELECT TOP 1 version, dirty FROM "` + ss.config.MigrationsTable + `"` + err = ss.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty) + switch { + case err == sql.ErrNoRows: + return database.NilVersion, false, nil + + case err != nil: + // FIXME: convert to MSSQL error + return 0, false, &database.Error{OrigErr: err, Query: []byte(query)} + + default: + return version, dirty, nil + } +} + +// Drop all tables from the database. +func (ss *MSSQL) Drop() error { + + // drop all referential integrity constraints + query := ` + DECLARE @Sql NVARCHAR(500) DECLARE @Cursor CURSOR + + SET @Cursor = CURSOR FAST_FORWARD FOR + SELECT DISTINCT sql = 'ALTER TABLE [' + tc2.TABLE_NAME + '] DROP [' + rc1.CONSTRAINT_NAME + ']' + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc1 + LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc2 ON tc2.CONSTRAINT_NAME =rc1.CONSTRAINT_NAME + + OPEN @Cursor FETCH NEXT FROM @Cursor INTO @Sql + + WHILE (@@FETCH_STATUS = 0) + BEGIN + Exec sp_executesql @Sql + FETCH NEXT FROM @Cursor INTO @Sql + END + + CLOSE @Cursor DEALLOCATE @Cursor` + + if _, err := ss.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + // drop the tables + query = `EXEC sp_MSforeachtable 'DROP TABLE ?'` + if _, err := ss.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + return nil +} + +func (ss *MSSQL) ensureVersionTable() (err error) { + if err = ss.Lock(); err != nil { + return err + } + + defer func() { + if e := ss.Unlock(); e != nil { + if err == nil { + err = e + } else { + err = multierror.Append(err, e) + } + } + }() + + query := `IF NOT EXISTS + (SELECT * + FROM sysobjects + WHERE id = object_id(N'[dbo].[` + ss.config.MigrationsTable + `]') + AND OBJECTPROPERTY(id, N'IsUserTable') = 1 + ) + CREATE TABLE ` + ss.config.MigrationsTable + ` ( version BIGINT PRIMARY KEY NOT NULL, dirty BIT NOT NULL );` + + if _, err = ss.conn.ExecContext(context.Background(), query); err != nil { + return &database.Error{OrigErr: err, Query: []byte(query)} + } + + return nil +} diff --git a/database/mssql/mssql_test.go b/database/mssql/mssql_test.go new file mode 100755 index 000000000..d26341d21 --- /dev/null +++ b/database/mssql/mssql_test.go @@ -0,0 +1,220 @@ +package mssql + +import ( + "context" + "database/sql" + sqldriver "database/sql/driver" + "fmt" + "log" + "strings" + "testing" + "time" + + "github.com/dhui/dktest" + "github.com/golang-migrate/migrate/v4" + + dt "github.com/golang-migrate/migrate/v4/database/testing" + "github.com/golang-migrate/migrate/v4/dktesting" + + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +const defaultPort = 1433 +const saPassword = "Root1234" + +var ( + opts = dktest.Options{ + Env: map[string]string{"ACCEPT_EULA": "Y", "SA_PASSWORD": saPassword, "MSSQL_PID": "Express"}, + PortRequired: true, ReadyFunc: isReady, PullTimeout: 2 * time.Minute, + } + // Container versions: https://mcr.microsoft.com/v2/mssql/server/tags/list + specs = []dktesting.ContainerSpec{ + {ImageName: "mcr.microsoft.com/mssql/server:2017-latest-ubuntu", Options: opts}, + {ImageName: "mcr.microsoft.com/mssql/server:2019-latest", Options: opts}, + } +) + +func msConnectionString(host, port string) string { + return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master", saPassword, host, port) +} + +func isReady(ctx context.Context, c dktest.ContainerInfo) bool { + ip, port, err := c.Port(defaultPort) + if err != nil { + return false + } + uri := msConnectionString(ip, port) + db, err := sql.Open("sqlserver", uri) + if err != nil { + return false + } + defer func() { + if err := db.Close(); err != nil { + log.Println("close error:", err) + } + }() + if err = db.PingContext(ctx); err != nil { + switch err { + case sqldriver.ErrBadConn: + return false + default: + fmt.Println(err) + } + return false + } + + return true +} + +func Test(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := msConnectionString(ip, port) + p := &MSSQL{} + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + + dt.Test(t, d, []byte("SELECT 1")) + }) +} + +func TestMigrate(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := msConnectionString(ip, port) + p := &MSSQL{} + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + + m, err := migrate.NewWithDatabaseInstance("file://./examples/migrations", "master", d) + if err != nil { + t.Fatal(err) + } + dt.TestMigrate(t, m, []byte("SELECT 1")) + }) +} + +func TestMultiStatement(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + addr := msConnectionString(ip, port) + ms := &MSSQL{} + d, err := ms.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLE bar (bar text);")); err != nil { + t.Fatalf("expected err to be nil, got %v", err) + } + + // make sure second table exists + var exists int + if err := d.(*MSSQL).conn.QueryRowContext(context.Background(), "SELECT COUNT(1) FROM information_schema.tables WHERE table_name = 'bar' AND table_schema = (SELECT schema_name()) AND table_catalog = (SELECT db_name())").Scan(&exists); err != nil { + t.Fatal(err) + } + if exists != 1 { + t.Fatalf("expected table bar to exist") + } + }) +} + +func TestErrorParsing(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.FirstPort() + if err != nil { + t.Fatal(err) + } + + addr := msConnectionString(ip, port) + p := &MSSQL{} + d, err := p.Open(addr) + if err != nil { + t.Fatal(err) + } + defer func() { + if err := d.Close(); err != nil { + t.Error(err) + } + }() + + wantErr := `migration failed: Unknown object type 'TABLEE' used in a CREATE, DROP, or ALTER statement. in line 1:` + + ` CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text); (details: mssql: Unknown object type ` + + `'TABLEE' used in a CREATE, DROP, or ALTER statement.)` + if err := d.Run(strings.NewReader("CREATE TABLE foo (foo text); CREATE TABLEE bar (bar text);")); err == nil { + t.Fatal("expected err but got nil") + } else if err.Error() != wantErr { + t.Fatalf("expected '%s' but got '%s'", wantErr, err.Error()) + } + }) +} + +func TestLockWorks(t *testing.T) { + dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) { + ip, port, err := c.Port(defaultPort) + if err != nil { + t.Fatal(err) + } + + addr := fmt.Sprintf("sqlserver://sa:%v@%v:%v?master", saPassword, ip, port) + p := &MSSQL{} + d, err := p.Open(addr) + if err != nil { + t.Fatalf("%v", err) + } + dt.Test(t, d, []byte("SELECT 1")) + + ms := d.(*MSSQL) + + err = ms.Lock() + if err != nil { + t.Fatal(err) + } + err = ms.Unlock() + if err != nil { + t.Fatal(err) + } + + // make sure the 2nd lock works (RELEASE_LOCK is very finicky) + err = ms.Lock() + if err != nil { + t.Fatal(err) + } + err = ms.Unlock() + if err != nil { + t.Fatal(err) + } + }) +} diff --git a/go.mod b/go.mod index 31c80de85..ae6e81879 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/cockroachdb/apd v1.1.0 // indirect github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c github.com/cznic/ql v1.2.0 + github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 github.com/dhui/dktest v0.3.0 github.com/docker/docker v0.7.3-0.20190108045446-77df18c24acf github.com/fsouza/fake-gcs-server v1.7.0 @@ -50,5 +51,4 @@ require ( google.golang.org/appengine v1.5.0 // indirect google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb google.golang.org/grpc v1.20.1 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 52d422433..55002b321 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKX 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/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= +github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/dhui/dktest v0.3.0 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I= github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc= github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU= @@ -200,6 +202,7 @@ go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -293,6 +296,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/cli/build_mssql.go b/internal/cli/build_mssql.go new file mode 100644 index 000000000..7d8f83ec2 --- /dev/null +++ b/internal/cli/build_mssql.go @@ -0,0 +1,7 @@ +// +build mssql + +package cli + +import ( + _ "github.com/golang-migrate/migrate/v4/database/mssql" +)