diff --git a/.circleci/config.yml b/.circleci/config.yml index 789dd9d4..8581fe9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,8 @@ jobs: - image: circleci/golang:1.11.5 environment: - TEST_DATABASE_POSTGRESQL=postgres://test:test@localhost:5432/sqlcon?sslmode=disable - - TEST_DATABASE_MYSQL=root:test@(localhost:3306)/mysql?parseTime=true + - TEST_DATABASE_MYSQL=mysql://root:test@(localhost:3306)/mysql?parseTime=true + - TEST_DATABASE_COCKROACHDB=cockroach://root@localhost:26257/defaultdb?sslmode=disable - image: postgres:9.5 environment: - POSTGRES_USER=test @@ -17,6 +18,8 @@ jobs: - image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORD=test + - image: cockroachdb/cockroach:v2.1.6 + command: start --insecure working_directory: /go/src/github.com/ory/sqlcon steps: - checkout diff --git a/dbal/canonicalize.go b/dbal/canonicalize.go index e6ba844e..9a73cc5c 100644 --- a/dbal/canonicalize.go +++ b/dbal/canonicalize.go @@ -6,26 +6,31 @@ const ( // DriverMySQL is the mysql driver name. DriverMySQL = "mysql" - // DriverPostgreSQL is the mysql driver name. + // DriverPostgreSQL is the postgres driver name. DriverPostgreSQL = "postgres" + // DriverCockroachDB is the cockroach driver name. + DriverCockroachDB = "cockroach" + // UnknownDriver is the driver name if the driver is unknown. UnknownDriver = "unknown" ) -// Canonicalize returns constants DriverMySQL, DriverPostgreSQL, UnknownDriver, depending on `database`. +// Canonicalize returns constants DriverMySQL, DriverPostgreSQL, DriverCockroachDB, UnknownDriver, depending on `database`. func Canonicalize(database string) string { switch database { case "mysql": return DriverMySQL case "pgx", "pq", "postgres": return DriverPostgreSQL + case "cockroach": + return DriverCockroachDB default: return UnknownDriver } } -// MustCanonicalize returns constants DriverMySQL, DriverPostgreSQL or fatals. +// MustCanonicalize returns constants DriverMySQL, DriverPostgreSQL, DriverCockroachDB or fatals. func MustCanonicalize(database string) string { d := Canonicalize(database) if d == UnknownDriver { diff --git a/dbal/connect.go b/dbal/connect.go index 3feabb01..0f1fc016 100644 --- a/dbal/connect.go +++ b/dbal/connect.go @@ -4,7 +4,6 @@ import ( "net/url" "time" - "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -12,7 +11,7 @@ import ( ) // Connect is a wrapper for connecting to different SQL drivers. -func Connect(db string, logger logrus.FieldLogger, memf func() error, sqlf func(db *sqlx.DB) error) error { +func Connect(db string, logger logrus.FieldLogger, memf func() error, sqlf func(db *sqlcon.DB) error) error { if db == "memory" { return memf() } else if db == "" { @@ -27,6 +26,8 @@ func Connect(db string, logger logrus.FieldLogger, memf func() error, sqlf func( switch u.Scheme { case "postgres": fallthrough + case "cockroach": + fallthrough case "mysql": c, err := sqlcon.NewSQLConnection(db, logger) if err != nil { diff --git a/dbal/migrate.go b/dbal/migrate.go index 77cfced5..7d11a9f9 100644 --- a/dbal/migrate.go +++ b/dbal/migrate.go @@ -70,7 +70,7 @@ func NewPackerMigrationSource(l logrus.FieldLogger, sources []string, loader fun var found bool for _, f := range filters { - if strings.Contains(source, f) { + if filepath.Dir(source) == f { found = true } } diff --git a/dbal/migratest/helper.go b/dbal/migratest/helper.go index 1d297ec9..50826537 100644 --- a/dbal/migratest/helper.go +++ b/dbal/migratest/helper.go @@ -5,13 +5,12 @@ import ( "sync" "testing" - "github.com/ory/x/dbal" - - "github.com/jmoiron/sqlx" "github.com/pborman/uuid" migrate "github.com/rubenv/sql-migrate" "github.com/stretchr/testify/require" + "github.com/ory/x/dbal" + "github.com/ory/x/sqlcon" "github.com/ory/x/sqlcon/dockertest" ) @@ -21,8 +20,8 @@ type MigrationSchemas []map[string]*dbal.PackrMigrationSource // RunPackrMigrationTests runs migration tests from packr migrations. func RunPackrMigrationTests( t *testing.T, schema, data MigrationSchemas, - init, cleanup func(*testing.T, *sqlx.DB), - runner func(*testing.T, *sqlx.DB, int, int, int), + init, cleanup func(*testing.T, *sqlcon.DB), + runner func(*testing.T, *sqlcon.DB, int, int, int), ) { if testing.Short() { t.SkipNow() @@ -30,7 +29,7 @@ func RunPackrMigrationTests( } var m sync.Mutex - var dbs = map[string]*sqlx.DB{} + var dbs = map[string]*sqlcon.DB{} var mid = uuid.New() dockertest.Parallel([]func(){ @@ -40,7 +39,7 @@ func RunPackrMigrationTests( t.Fatalf("Could not connect to database: %v", err) } m.Lock() - dbs["postgres"] = db + dbs["postgres"] = sqlcon.NewDB(db, "postgres") m.Unlock() }, func() { @@ -49,7 +48,16 @@ func RunPackrMigrationTests( t.Fatalf("Could not connect to database: %v", err) } m.Lock() - dbs["mysql"] = db + dbs["mysql"] = sqlcon.NewDB(db, "mysql") + m.Unlock() + }, + func() { + db, err := dockertest.ConnectToTestCockroachDB() + if err != nil { + t.Fatalf("Could not connect to database: %v", err) + } + m.Lock() + dbs["cockroach"] = sqlcon.NewDB(db, "cockroach") m.Unlock() }, }) @@ -68,7 +76,7 @@ func RunPackrMigrationTests( for step := 0; step < steps; step++ { t.Run(fmt.Sprintf("up=%d", step), func(t *testing.T) { migrate.SetTable(fmt.Sprintf("%s_%d", mid, sk)) - n, err := migrate.ExecMax(db.DB, db.DriverName(), ss[name], migrate.Up, 1) + n, err := migrate.ExecMax(db.DB.DB, db.Dialect(), ss[name], migrate.Up, 1) require.NoError(t, err) require.Equal(t, n, 1, sk) @@ -79,7 +87,7 @@ func RunPackrMigrationTests( } migrate.SetTable(fmt.Sprintf("%s_%d_data", mid, sk)) - n, err = migrate.ExecMax(db.DB, db.DriverName(), data[sk][name], migrate.Up, 1) + n, err = migrate.ExecMax(db.DB.DB, db.Dialect(), data[sk][name], migrate.Up, 1) require.NoError(t, err) require.Equal(t, n, 1) }) @@ -103,7 +111,7 @@ func RunPackrMigrationTests( migrate.SetTable(fmt.Sprintf("%s_%d", mid, sk)) for step := 0; step < steps; step++ { t.Run(fmt.Sprintf("down=%d", step), func(t *testing.T) { - n, err := migrate.ExecMax(db.DB, db.DriverName(), ss[name], migrate.Down, 1) + n, err := migrate.ExecMax(db.DB.DB, db.Dialect(), ss[name], migrate.Down, 1) require.NoError(t, err) require.Equal(t, n, 1) }) diff --git a/sqlcon/connector.go b/sqlcon/connector.go index b2e759ee..e0b5e895 100644 --- a/sqlcon/connector.go +++ b/sqlcon/connector.go @@ -22,6 +22,7 @@ package sqlcon import ( + "context" "database/sql" "fmt" "net/url" @@ -43,12 +44,55 @@ import ( // SQLConnection represents a connection to a SQL database. type SQLConnection struct { - db *sqlx.DB + db *DB URL *url.URL L logrus.FieldLogger options } +// DB represents a wrapped sqlx.DB with own defined driver name. +type DB struct { + *sqlx.DB + driverName string + bindType int +} + +// NewDB returns a new DB +func NewDB(sqlxDB *sqlx.DB, driverName string) *DB { + db := &DB{DB: sqlxDB, driverName: driverName} + if driverName != "cockroach" { + db.bindType = sqlx.BindType(driverName) + } else { + db.bindType = sqlx.DOLLAR + } + return db +} + +// Rebind wraps sqlx.DB.Rebind +func (d *DB) Rebind(query string) string { + return sqlx.Rebind(d.bindType, query) +} + +// NamedExecContext wraps sqlx.DB.NamedExecContext +func (d *DB) NamedExecContext(ctx context.Context, query string, arg interface{}) (sql.Result, error) { + return d.DB.NamedExecContext(ctx, d.Rebind(query), arg) +} + +// DriverName returns db.driverName +func (d *DB) DriverName() string { + return d.driverName +} + +// Dialect returns sql.DB.DriverName +func (d *DB) Dialect() string { + dialect := d.DB.DriverName() + switch dialect { + case "pgx", "pq": + dialect = "postgres" + } + return dialect +} + // NewSQLConnection returns a new SQLConnection. func NewSQLConnection(db string, l logrus.FieldLogger, opts ...OptionModifier) (*SQLConnection, error) { u, err := url.Parse(db) @@ -92,7 +136,7 @@ func cleanURLQuery(c *url.URL) *url.URL { } // GetDatabaseRetry tries to connect to a database and fails after failAfter. -func (c *SQLConnection) GetDatabaseRetry(maxWait time.Duration, failAfter time.Duration) (*sqlx.DB, error) { +func (c *SQLConnection) GetDatabaseRetry(maxWait time.Duration, failAfter time.Duration) (*DB, error) { if err := resilience.Retry(c.L, maxWait, failAfter, func() (err error) { c.db, err = c.GetDatabase() if err != nil { @@ -107,7 +151,7 @@ func (c *SQLConnection) GetDatabaseRetry(maxWait time.Duration, failAfter time.D } // GetDatabase retrusn a database instance. -func (c *SQLConnection) GetDatabase() (*sqlx.DB, error) { +func (c *SQLConnection) GetDatabase() (*DB, error) { if c.db != nil { return c.db, nil } @@ -123,12 +167,15 @@ func (c *SQLConnection) GetDatabase() (*sqlx.DB, error) { c.L.Infof("Connecting with %s", c.URL.Scheme+"://*:*@"+c.URL.Host+c.URL.Path+"?"+clean.RawQuery) u := connectionString(clean) + if registeredDriver == "cockroach" { + registeredDriver = "postgres" + } db, err := sql.Open(registeredDriver, u) if err != nil { return nil, errors.Wrapf(err, "could not open SQL connection") } - c.db = sqlx.NewDb(db, clean.Scheme) + c.db = NewDB(sqlx.NewDb(db, registeredDriver), clean.Scheme) if err := c.db.Ping(); err != nil { return nil, errors.Wrapf(err, "could not ping SQL connection") } @@ -204,6 +251,9 @@ func connectionString(clean *url.URL) string { if clean.Scheme == "mysql" { u = strings.Replace(u, "mysql://", "", -1) } + if clean.Scheme == "cockroach" { + u = strings.Replace(u, "cockroach://", "postgres://", 1) + } return u } @@ -229,6 +279,11 @@ func (c *SQLConnection) registerDriver() (string, error) { // and does not satisfy the driver.Driver interface. sql.Register(driverName, instrumentedsql.WrapDriver(&pq.Driver{}, tracingOpts...)) + case "cockroach": + // Why does this have to be a pointer? Because the Open method for postgres has a pointer receiver + // and does not satisfy the driver.Driver interface. + sql.Register(driverName, + instrumentedsql.WrapDriver(&pq.Driver{}, tracingOpts...)) default: return "", fmt.Errorf("unsupported scheme (%s) in DSN", c.URL.Scheme) } diff --git a/sqlcon/connector_test.go b/sqlcon/connector_test.go index 51429611..7d34e222 100644 --- a/sqlcon/connector_test.go +++ b/sqlcon/connector_test.go @@ -34,7 +34,7 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" - "github.com/opentracing/opentracing-go" + opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/mocktracer" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -45,9 +45,10 @@ import ( ) var ( - mysqlURL *url.URL - postgresURL *url.URL - resources []*dockertest.Resource + mysqlURL *url.URL + postgresURL *url.URL + cockroachURL *url.URL + resources []*dockertest.Resource ) func TestMain(m *testing.M) { @@ -56,6 +57,7 @@ func TestMain(m *testing.M) { dockertestd.Parallel([]func(){ bootstrapMySQL, bootstrapPostgres, + bootstrapCockroach, }) } @@ -79,8 +81,9 @@ func TestDistributedTracing(t *testing.T) { } databases := map[string]string{ - "mysql": mysqlURL.String(), - "postgres": postgresURL.String(), + "mysql": mysqlURL.String(), + "postgres": postgresURL.String(), + "cockroach": cockroachURL.String(), } for driver, dsn := range databases { @@ -144,9 +147,6 @@ func TestDistributedTracing(t *testing.T) { } func TestRegisterDriver(t *testing.T) { - unsupportedDSN := "unsupported://unsupported:secret@localhost:1337/mydb" - supportedDSN := "mysql://foo@bar:baz@qux/db" - for _, testCase := range []struct { description string sqlConnection *SQLConnection @@ -155,20 +155,20 @@ func TestRegisterDriver(t *testing.T) { }{ { description: "should return error if supplied DSN is unsupported for tracing", - sqlConnection: mustSQL(t, unsupportedDSN, WithDistributedTracing()), + sqlConnection: mustSQL(t, "unsupported://unsupported:secret@localhost:1337/mydb", WithDistributedTracing()), expectedDriverName: "", shouldError: true, }, { description: "should return registered driver name if supplied DSN is valid for tracing", - sqlConnection: mustSQL(t, supportedDSN, WithDistributedTracing()), + sqlConnection: mustSQL(t, "mysql://foo@bar:baz@qux/db", WithDistributedTracing()), expectedDriverName: "instrumented-sql-driver", shouldError: false, }, { - description: "should return registered driver name if tracing is NOT configured", - sqlConnection: mustSQL(t, supportedDSN), - expectedDriverName: "mysql", + description: "should return cockroach driver if a valid cockroach DSN is supplied", + sqlConnection: mustSQL(t, "cockroach://foo@bar:baz@qux/db"), + expectedDriverName: "cockroach", shouldError: false, }, } { @@ -243,6 +243,18 @@ func TestSQLConnection(t *testing.T) { d: "pg max_conn_lifetime", s: mustSQL(t, merge(postgresURL, map[string]string{"max_conn_lifetime": "1h", "max_idle_conns": "10", "max_conns": "10"}).String()), }, + { + d: "crdb raw", + s: mustSQL(t, cockroachURL.String()), + }, + { + d: "crdb max_conn_lifetime", + s: mustSQL(t, merge(cockroachURL, map[string]string{"max_conn_lifetime": "1h"}).String()), + }, + { + d: "crdb max_conn_lifetime", + s: mustSQL(t, merge(cockroachURL, map[string]string{"max_conn_lifetime": "1h", "max_idle_conns": "10", "max_conns": "10"}).String()), + }, } { t.Run(fmt.Sprintf("case=%s", tc.d), func(t *testing.T) { tc.s.L = logrus.New() @@ -276,7 +288,7 @@ func killAll() { func bootstrapMySQL() { if uu := os.Getenv("TEST_DATABASE_MYSQL"); uu != "" { log.Println("Found mysql test database config, skipping dockertest...") - _, err := sqlx.Open("postgres", uu) + _, err := sqlx.Open("mysql", uu) if err != nil { log.Fatalf("Could not connect to bootstrapped database: %s", err) } @@ -330,6 +342,38 @@ func bootstrapPostgres() { postgresURL = u } +func bootstrapCockroach() { + if uu := os.Getenv("TEST_DATABASE_COCKROACHDB"); uu != "" { + log.Println("Found cockroachdb test database config, skipping dockertest...") + _, err := sqlx.Open("postgres", uu) + if err != nil { + log.Fatalf("Could not connect to bootstrapped database: %s", err) + } + u, _ := url.Parse(strings.Replace(uu, "postgres://", "cockroach://", 1)) + cockroachURL = u + return + } + + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not Connect to docker: %s", err) + } + + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "cockroachdb/cockroach", + Tag: "v2.1.6", + Cmd: []string{"start --insecure"}, + }) + if err != nil { + log.Fatalf("Could not start resource: %s", err) + } + + urls := bootstrap("postgres://root@localhost:%s/defaultdb?sslmode=disable", "26257/tcp", "postgres", pool, resource) + resources = append(resources, resource) + u, _ := url.Parse(strings.Replace(urls, "postgres://", "cockroach://", 1)) + cockroachURL = u +} + func bootstrap(u, port, driver string, pool *dockertest.Pool, resource *dockertest.Resource) (urls string) { if err := pool.Retry(func() error { var err error diff --git a/sqlcon/dockertest/test_helper.go b/sqlcon/dockertest/test_helper.go index 6cba2441..9edfa251 100644 --- a/sqlcon/dockertest/test_helper.go +++ b/sqlcon/dockertest/test_helper.go @@ -3,7 +3,9 @@ package dockertest import ( "fmt" "log" + "net/url" "os" + "strings" "sync" "time" @@ -64,13 +66,23 @@ func Parallel(fs []func()) { wg.Wait() } -func connect(driver, dsn string) (db *sqlx.DB, err error) { +func connect(dialect, driver, dsn string) (db *sqlx.DB, err error) { + clean, err := url.Parse(dsn) + if err != nil { + return nil, err + } + if clean.Scheme == "mysql" { + dsn = strings.Replace(dsn, "mysql://", "", -1) + } + if clean.Scheme == "cockroach" { + dsn = strings.Replace(dsn, "cockroach://", "postgres://", 1) + } err = resilience.Retry( logrus.New(), time.Second*5, time.Minute*5, func() (err error) { - db, err = sqlx.Open(driver, dsn) + db, err = sqlx.Open(dialect, dsn) if err != nil { log.Printf("Connecting to database %s failed: %s", driver, err) return err @@ -93,9 +105,9 @@ func connect(driver, dsn string) (db *sqlx.DB, err error) { // ConnectToTestPostgreSQL connects to a PostgreSQL database. func ConnectToTestPostgreSQL() (*sqlx.DB, error) { - if url := os.Getenv("TEST_DATABASE_POSTGRESQL"); url != "" { + if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" { log.Println("Found postgresql test database config, skipping dockertest...") - return connect("postgres", url) + return connect("postgres", "postgres", dsn) } pool, err := dockertest.NewPool("") @@ -114,9 +126,9 @@ func ConnectToTestPostgreSQL() (*sqlx.DB, error) { // ConnectToTestMySQL connects to a MySQL database. func ConnectToTestMySQL() (*sqlx.DB, error) { - if url := os.Getenv("TEST_DATABASE_MYSQL"); url != "" { + if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" { log.Println("Found mysql test database config, skipping dockertest...") - return connect("mysql", url) + return connect("mysql", "mysql", dsn) } pool, err := dockertest.NewPool("") @@ -133,6 +145,31 @@ func ConnectToTestMySQL() (*sqlx.DB, error) { return db, nil } +// ConnectToTestCockroachDB connects to a CockroachDB database. +func ConnectToTestCockroachDB() (*sqlx.DB, error) { + if dsn := os.Getenv("TEST_DATABASE_COCKROACHDB"); dsn != "" { + log.Println("Found cockroachdb test database config, skipping dockertest...") + return connect("postgres", "cockroach", dsn) + } + + pool, err := dockertest.NewPool("") + if err != nil { + return nil, errors.Wrap(err, "Could not connect to docker") + } + + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "cockroachdb/cockroach", + Tag: "v2.1.6", + Cmd: []string{"start --insecure"}, + }) + if err != nil { + return nil, errors.Wrap(err, "Could not start resource") + } + + db := bootstrap("postgres://root@localhost:%s/defaultdb?sslmode=disable", "26257/tcp", "postgres", pool, resource) + return db, nil +} + func bootstrap(u, port, d string, pool *dockertest.Pool, resource *dockertest.Resource) (db *sqlx.DB) { if err := resilience.Retry(logrus.New(), time.Second*5, time.Minute*5, func() error { var err error diff --git a/sqlcon/message.go b/sqlcon/message.go index e2b4fcec..52791266 100644 --- a/sqlcon/message.go +++ b/sqlcon/message.go @@ -11,7 +11,7 @@ func HelpMessage() string { - Changes are kept after process death (persistent storage): - - SQL Databases: Officially, PostgreSQL and MySQL are supported. This project works best with PostgreSQL. + - SQL Databases: Officially, PostgreSQL, MySQL and CockroachDB are supported. This project works best with PostgreSQL. - PostgreSQL: If DATABASE_URL is a DSN starting with postgres://, PostgreSQL will be used as storage backend. Example: DATABASE_URL=postgres://user:password@host:123/database @@ -58,6 +58,27 @@ func HelpMessage() string { ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true&writeTimeout=123s + - CockroachDB: If DATABASE_URL is a DSN starting with cockroach://, CockroachDB will be used as storage backend. + Example: DATABASE_URL=cockroach://user:password@host:123/database + + Additionally, the following query/DSN parameters are supported: + * sslmode (string): Whether or not to use SSL (default is require) + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + * fallback_application_name (string): An application_name to fall back to if one isn't provided. + * connect_timeout (number): Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + * sslcert (string): Cert file location. The file must contain PEM encoded data. + * sslkey (string): Key file location. The file must contain PEM encoded data. + * sslrootcert (string): The location of the root certificate file. The file + must contain PEM encoded data. + Example: DATABASE_URL=cockroach://user:password@host:123/database?sslmode=verify-full + The following settings can be configured using URL query parameters (postgres://.../database?max_conns=1): * max_conns (number): Sets the maximum number of open connections to the database. Defaults to the number of CPU cores times 2. * max_idle_conns (number): Sets the maximum number of connections in the idle. Defaults to the number of CPU cores. diff --git a/sqlcon/migrate.go b/sqlcon/migrate.go index 204c3eb9..15a5b05e 100644 --- a/sqlcon/migrate.go +++ b/sqlcon/migrate.go @@ -7,7 +7,6 @@ import ( "github.com/ory/x/viperx" - "github.com/jmoiron/sqlx" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -15,7 +14,7 @@ import ( // SchemaCreator is an interface that allows SQL schemas to be created and migrated. type SchemaCreator interface { // CreateSchemas migrates or creates one or more SQL schemas. - CreateSchemas(db *sqlx.DB) (int, error) + CreateSchemas(db *DB) (int, error) } // MigratorSQLCmd returns a *cobra.Command executing SQL schema migrations. @@ -30,7 +29,7 @@ It is recommended to run this command close to the SQL instance (e.g. same subne This decreases risk of failure and decreases time required. We strongly advise to create a back up before running this command against an existing database. The migration command -may lock MySQL databases, depending on table sizes. This is not the case for PostgreSQL databases. +may lock MySQL databases, depending on table sizes. This is not the case for PostgreSQL and CockroachDB databases. Examples: @@ -57,8 +56,8 @@ Examples: logger.WithError(err).WithField("dsn", db).Fatal(`Unable to parse configuration item "dsn", make sure it has the right format`) } - if dbu.Scheme != "postgres" && dbu.Scheme != "mysql" { - logger.WithField("dsn", dbu.Scheme+"://*:*@"+dbu.Host+dbu.Path+"?"+dbu.RawQuery).Fatal("Migrations can only be run against PostgreSQL or MySQL databases") + if dbu.Scheme != "postgres" && dbu.Scheme != "mysql" && dbu.Scheme != "cockroach" { + logger.WithField("dsn", dbu.Scheme+"://*:*@"+dbu.Host+dbu.Path+"?"+dbu.RawQuery).Fatal("Migrations can only be run against PostgreSQL, MySQL or CockroachDB databases") } sdb, err := NewSQLConnection(db, logger)