whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# 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). |
|
||||
| `useMsi` | | `true` - Use Azure MSI Authentication for connecting to Sql Server. Must be running from an Azure VM/an instance with MSI enabled. `false` - Use password authentication (Default). See [here for Azure MSI Auth details](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-connect-msi). NOTE: Since this cannot be tested locally, this is not officially supported.
|
||||
|
||||
See https://github.com/microsoft/go-mssqldb for full parameter list.
|
||||
|
||||
## Driver Support
|
||||
|
||||
### Which go-mssqldb driver to us?
|
||||
|
||||
Please note that the deprecated `mssql` driver is not supported. Please use the newer `sqlserver` driver.
|
||||
See https://github.com/microsoft/go-mssqldb#deprecated for more information.
|
||||
|
||||
### Official Support by migrate
|
||||
|
||||
Versions of MS SQL Server 2019 newer than CTP3.1 are not officially supported since there are issues testing against the Docker image.
|
||||
For more info, see: https://github.com/golang-migrate/migrate/issues/160#issuecomment-522433269
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE users (
|
||||
user_id integer unique,
|
||||
name varchar(40),
|
||||
email varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS city;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE users ADD city varchar(100);
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS users_email_index;
|
||||
+3
@@ -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.
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS books;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE books (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
author varchar(40)
|
||||
);
|
||||
+1
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE movies (
|
||||
user_id integer,
|
||||
name varchar(40),
|
||||
director varchar(40)
|
||||
);
|
||||
+1
@@ -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.
|
||||
+1
@@ -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.
|
||||
+1
@@ -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.
|
||||
+1
@@ -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.
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
nurl "net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
mssql "github.com/microsoft/go-mssqldb" // mssql support
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Register("sqlserver", &SQLServer{})
|
||||
}
|
||||
|
||||
// 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")
|
||||
ErrMultipleAuthOptionsPassed = fmt.Errorf("both password and useMsi=true were passed.")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// SQL Server connection
|
||||
type SQLServer struct {
|
||||
// Locking and unlocking need to use the same connection
|
||||
conn *sql.Conn
|
||||
db *sql.DB
|
||||
isLocked atomic.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.
|
||||
//
|
||||
// Note that the deprecated `mssql` driver is not supported. Please use the newer `sqlserver` driver.
|
||||
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
|
||||
}
|
||||
|
||||
if config.DatabaseName == "" {
|
||||
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
|
||||
}
|
||||
|
||||
if config.SchemaName == "" {
|
||||
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 := &SQLServer{
|
||||
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 *SQLServer) Open(url string) (database.Driver, error) {
|
||||
purl, err := nurl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
useMsiParam := purl.Query().Get("useMsi")
|
||||
useMsi := false
|
||||
if len(useMsiParam) > 0 {
|
||||
useMsi, err = strconv.ParseBool(useMsiParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if _, isPasswordSet := purl.User.Password(); useMsi && isPasswordSet {
|
||||
return nil, ErrMultipleAuthOptionsPassed
|
||||
}
|
||||
|
||||
filteredURL := migrate.FilterCustomQuery(purl).String()
|
||||
|
||||
var db *sql.DB
|
||||
if useMsi {
|
||||
resource := getAADResourceFromServerUri(purl)
|
||||
tokenProvider, err := getMSITokenProvider(resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connector, err := mssql.NewAccessTokenConnector(
|
||||
filteredURL, tokenProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db = sql.OpenDB(connector)
|
||||
|
||||
} else {
|
||||
db, err = sql.Open("sqlserver", filteredURL)
|
||||
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 *SQLServer) 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 *SQLServer) Lock() error {
|
||||
return database.CasRestoreOnErr(&ss.isLocked, false, true, database.ErrLocked, func() error {
|
||||
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 = @p1, @LockMode = 'Update', @LockOwner = 'Session', @LockTimeout = 0`
|
||||
|
||||
var status mssql.ReturnStatus
|
||||
if _, err = ss.conn.ExecContext(context.Background(), query, aid, &status); err == nil && status > -1 {
|
||||
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 *SQLServer) Unlock() error {
|
||||
return database.CasRestoreOnErr(&ss.isLocked, true, false, database.ErrNotLocked, func() error {
|
||||
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 = @p1, @LockOwner = 'Session'`
|
||||
if _, err := ss.conn.ExecContext(context.Background(), query, aid); err != nil {
|
||||
return &database.Error{OrigErr: err, Query: []byte(query)}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Run the migrations for the database
|
||||
func (ss *SQLServer) Run(migration io.Reader) error {
|
||||
migr, err := io.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 *SQLServer) 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.getMigrationTable()
|
||||
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)}
|
||||
}
|
||||
|
||||
// Also re-write the schema version for nil dirty versions to prevent
|
||||
// empty schema version for failed down migration on the first migration
|
||||
// See: https://github.com/golang-migrate/migrate/issues/330
|
||||
if version >= 0 || (version == database.NilVersion && dirty) {
|
||||
var dirtyBit int
|
||||
if dirty {
|
||||
dirtyBit = 1
|
||||
}
|
||||
query = `INSERT INTO ` + ss.getMigrationTable() + ` (version, dirty) VALUES (@p1, @p2)`
|
||||
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 *SQLServer) Version() (version int, dirty bool, err error) {
|
||||
query := `SELECT TOP 1 version, dirty FROM ` + ss.getMigrationTable()
|
||||
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 *SQLServer) 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 *SQLServer) 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'` + ss.getMigrationTable() + `')
|
||||
AND OBJECTPROPERTY(id, N'IsUserTable') = 1
|
||||
)
|
||||
CREATE TABLE ` + ss.getMigrationTable() + ` ( 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
|
||||
}
|
||||
|
||||
func (ss *SQLServer) getMigrationTable() string {
|
||||
return fmt.Sprintf("[%s].[%s]", ss.config.SchemaName, ss.config.MigrationsTable)
|
||||
}
|
||||
|
||||
func getMSITokenProvider(resource string) (func() (string, error), error) {
|
||||
msi, err := adal.NewServicePrincipalTokenFromManagedIdentity(resource, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() (string, error) {
|
||||
err := msi.EnsureFresh()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := msi.OAuthToken()
|
||||
return token, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The sql server resource can change across clouds so get it
|
||||
// dynamically based on the server uri.
|
||||
// ex. <server name>.database.windows.net -> https://database.windows.net
|
||||
func getAADResourceFromServerUri(purl *nurl.URL) string {
|
||||
return fmt.Sprintf("%s%s", "https://", strings.Join(strings.Split(purl.Hostname(), ".")[1:], "."))
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
package sqlserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
sqldriver "database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dhui/dktest"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"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 (
|
||||
sqlEdgeOpts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "MSSQL_SA_PASSWORD": saPassword},
|
||||
PortBindings: map[nat.Port][]nat.PortBinding{
|
||||
nat.Port(fmt.Sprintf("%d/tcp", defaultPort)): {
|
||||
nat.PortBinding{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: "0/tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
PortRequired: true, ReadyFunc: isReady, PullTimeout: 2 * time.Minute,
|
||||
}
|
||||
sqlServerOpts = dktest.Options{
|
||||
Env: map[string]string{"ACCEPT_EULA": "Y", "MSSQL_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/azure-sql-edge:latest", Options: sqlEdgeOpts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2017-latest", Options: sqlServerOpts},
|
||||
{ImageName: "mcr.microsoft.com/mssql/server:2019-latest", Options: sqlServerOpts},
|
||||
}
|
||||
)
|
||||
|
||||
func msConnectionString(host, port string) string {
|
||||
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master", saPassword, host, port)
|
||||
}
|
||||
|
||||
func msConnectionStringMsiWithPassword(host, port string, useMsi bool) string {
|
||||
return fmt.Sprintf("sqlserver://sa:%v@%v:%v?database=master&useMsi=%t", saPassword, host, port, useMsi)
|
||||
}
|
||||
|
||||
func msConnectionStringMsi(host, port string, useMsi bool) string {
|
||||
return fmt.Sprintf("sqlserver://sa@%v:%v?database=master&useMsi=%t", host, port, useMsi)
|
||||
}
|
||||
|
||||
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 SkipIfUnsupportedArch(t *testing.T, c dktest.ContainerInfo) {
|
||||
if strings.Contains(c.ImageName, "mssql") && !strings.HasPrefix(runtime.GOARCH, "amd") {
|
||||
t.Skipf("Image %s is not supported on arch %s", c.ImageName, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
t.Run("test", test)
|
||||
t.Run("testMigrate", testMigrate)
|
||||
t.Run("testMultiStatement", testMultiStatement)
|
||||
t.Run("testErrorParsing", testErrorParsing)
|
||||
t.Run("testLockWorks", testLockWorks)
|
||||
t.Run("testMsiTrue", testMsiTrue)
|
||||
t.Run("testOpenWithPasswordAndMSI", testOpenWithPasswordAndMSI)
|
||||
t.Run("testMsiFalse", testMsiFalse)
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, spec := range specs {
|
||||
t.Log("Cleaning up ", spec.ImageName)
|
||||
if err := spec.Cleanup(); err != nil {
|
||||
t.Error("Error removing ", spec.ImageName, "error:", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func test(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionString(ip, port)
|
||||
p := &SQLServer{}
|
||||
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) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionString(ip, port)
|
||||
p := &SQLServer{}
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func testMultiStatement(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionString(ip, port)
|
||||
ms := &SQLServer{}
|
||||
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.(*SQLServer).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) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionString(ip, port)
|
||||
|
||||
p := &SQLServer{}
|
||||
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) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
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 := &SQLServer{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
|
||||
ms := d.(*SQLServer)
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testMsiTrue(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionStringMsi(ip, port, true)
|
||||
p := &SQLServer{}
|
||||
_, err = p.Open(addr)
|
||||
if err == nil {
|
||||
t.Fatal("MSI should fail when not running in an Azure context.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testOpenWithPasswordAndMSI(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionStringMsiWithPassword(ip, port, true)
|
||||
p := &SQLServer{}
|
||||
_, err = p.Open(addr)
|
||||
if err == nil {
|
||||
t.Fatal("Open should fail when both password and useMsi=true are passed.")
|
||||
}
|
||||
|
||||
addr = msConnectionStringMsiWithPassword(ip, port, false)
|
||||
p = &SQLServer{}
|
||||
d, err := p.Open(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := d.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
dt.Test(t, d, []byte("SELECT 1"))
|
||||
})
|
||||
}
|
||||
|
||||
func testMsiFalse(t *testing.T) {
|
||||
dktesting.ParallelTest(t, specs, func(t *testing.T, c dktest.ContainerInfo) {
|
||||
SkipIfUnsupportedArch(t, c)
|
||||
ip, port, err := c.Port(defaultPort)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
addr := msConnectionStringMsi(ip, port, false)
|
||||
p := &SQLServer{}
|
||||
_, err = p.Open(addr)
|
||||
if err == nil {
|
||||
t.Fatal("Open should fail since no password was passed and useMsi is false.")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user