mirror of https://github.com/status-im/migrate.git
commit
7b3cd164d7
|
@ -4,4 +4,5 @@ cli/cli
|
||||||
cli/migrate
|
cli/migrate
|
||||||
.coverage
|
.coverage
|
||||||
.godoc.pid
|
.godoc.pid
|
||||||
vendor/
|
vendor/
|
||||||
|
.vscode/
|
||||||
|
|
|
@ -8,7 +8,7 @@ WORKDIR /go/src/github.com/golang-migrate/migrate
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|
||||||
ENV GO111MODULE=on
|
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"
|
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
|
RUN go build -a -o build/migrate.linux-386 -ldflags="-X main.Version=${VERSION}" -tags "$DATABASES $SOURCES" ./cmd/migrate
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -1,5 +1,5 @@
|
||||||
SOURCE ?= file go_bindata github aws_s3 google_cloud_storage godoc_vfs gitlab
|
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-)
|
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
|
||||||
TEST_FLAGS ?=
|
TEST_FLAGS ?=
|
||||||
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
|
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
|
||||||
|
|
|
@ -37,6 +37,7 @@ Database drivers run migrations. [Add a new database?](database/driver.go)
|
||||||
* [CockroachDB](database/cockroachdb)
|
* [CockroachDB](database/cockroachdb)
|
||||||
* [ClickHouse](database/clickhouse)
|
* [ClickHouse](database/clickhouse)
|
||||||
* [Firebird](database/firebird) ([todo #49](https://github.com/golang-migrate/migrate/issues/49))
|
* [Firebird](database/firebird) ([todo #49](https://github.com/golang-migrate/migrate/issues/49))
|
||||||
|
* [Postgres](database/postgres)
|
||||||
|
|
||||||
### Database URLs
|
### Database URLs
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS users;
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
user_id integer unique,
|
||||||
|
name varchar(40),
|
||||||
|
email varchar(40)
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE users ADD city varchar(100);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
DROP INDEX IF EXISTS users_email_index;
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS books;
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE books (
|
||||||
|
user_id integer,
|
||||||
|
name varchar(40),
|
||||||
|
author varchar(40)
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS movies;
|
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE movies (
|
||||||
|
user_id integer,
|
||||||
|
name varchar(40),
|
||||||
|
director varchar(40)
|
||||||
|
);
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/cockroachdb/apd v1.1.0 // indirect
|
github.com/cockroachdb/apd v1.1.0 // indirect
|
||||||
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c
|
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c
|
||||||
github.com/cznic/ql v1.2.0
|
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/dhui/dktest v0.3.0
|
||||||
github.com/docker/docker v0.7.3-0.20190108045446-77df18c24acf
|
github.com/docker/docker v0.7.3-0.20190108045446-77df18c24acf
|
||||||
github.com/fsouza/fake-gcs-server v1.7.0
|
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/appengine v1.5.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb
|
||||||
google.golang.org/grpc v1.20.1 // indirect
|
google.golang.org/grpc v1.20.1 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
4
go.sum
4
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:kwX5a7EkLcjo7VpsPQSYJcKGbXBXdjI9FGjuUj1jn6I=
|
||||||
github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
|
github.com/dhui/dktest v0.3.0/go.mod h1:cyzIUfGsBEbZ6BT7tnXqAShHSXCZhSNmFl70sZ7c1yc=
|
||||||
github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU=
|
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=
|
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-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-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 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
|
||||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
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=
|
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/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/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.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 h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
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=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
// +build mssql
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/mssql"
|
||||||
|
)
|
Loading…
Reference in New Issue