diff --git a/Makefile b/Makefile index 9082e15..01c2372 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ lint: wait: dist/dbmate-linux-amd64 -e MYSQL_URL wait dist/dbmate-linux-amd64 -e POSTGRESQL_URL wait + dist/dbmate-linux-amd64 -e CLICKHOUSE_URL wait .PHONY: clean clean: diff --git a/README.md b/README.md index 0eb4ea1..b536350 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For a comparison between dbmate and other popular database schema migration tool ## Features -* Supports MySQL, PostgreSQL, and SQLite. +* Supports MySQL, PostgreSQL, SQLite, and ClickHouse. * Uses plain SQL for writing schema migrations. * Migrations are timestamp-versioned, to avoid version number conflicts with multiple developers. * Migrations are run atomically inside a transaction. @@ -117,7 +117,7 @@ DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_development?sslmode=disab protocol://username:password@host:port/database_name?options ``` -* `protocol` must be one of `mysql`, `postgres`, `postgresql`, `sqlite`, `sqlite3` +* `protocol` must be one of `mysql`, `postgres`, `postgresql`, `sqlite`, `sqlite3`, `clickhouse` * `host` can be either a hostname or IP address * `options` are driver-specific (refer to the underlying Go SQL drivers if you wish to use these) @@ -161,6 +161,20 @@ To specify an absolute path, add an additional forward slash to the path. The fo DATABASE_URL="sqlite:////tmp/database_name.sqlite3" ``` +**ClickHouse** + +```sh +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name" +``` + +or + +```sh +DATABASE_URL="clickhouse://127.0.0.1:9000?username=username&password=password&database=database_name" +``` + +[See other supported connection options](https://github.com/ClickHouse/clickhouse-go#dsn). + ### Creating Migrations To create a new migration, run `dbmate new create_users_table`. You can name the migration anything you like. This will create a file `db/migrations/20151127184807_create_users_table.sql` in the current directory: @@ -333,7 +347,7 @@ Why another database schema migration tool? Dbmate was inspired by many other to | | [goose](https://bitbucket.org/liamstask/goose/) | [sql-migrate](https://github.com/rubenv/sql-migrate) | [golang-migrate/migrate](https://github.com/golang-migrate/migrate) | [activerecord](http://guides.rubyonrails.org/active_record_migrations.html) | [sequelize](http://docs.sequelizejs.com/manual/tutorial/migrations.html) | [dbmate](https://github.com/amacneil/dbmate) | | --- |:---:|:---:|:---:|:---:|:---:|:---:| -| **Features** ||||||| +| **Features** | |Plain SQL migration files|:white_check_mark:|:white_check_mark:|:white_check_mark:|||:white_check_mark:| |Support for creating and dropping databases||||:white_check_mark:||:white_check_mark:| |Support for saving schema dump files||||:white_check_mark:||:white_check_mark:| @@ -343,10 +357,11 @@ Why another database schema migration tool? Dbmate was inspired by many other to |Automatically load .env file||||||:white_check_mark:| |No separate configuration file||||:white_check_mark:|:white_check_mark:|:white_check_mark:| |Language/framework independent|:eight_pointed_black_star:|:eight_pointed_black_star:|:eight_pointed_black_star:|||:white_check_mark:| -| **Drivers** ||||||| +| **Drivers** | |PostgreSQL|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| |MySQL|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| |SQLite|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| +|CliсkHouse|||:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| > :eight_pointed_black_star: In theory these tools could be used with other languages, but a Go development environment is required because binary builds are not provided. diff --git a/docker-compose.yml b/docker-compose.yml index 0f81590..57b9996 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,9 +7,11 @@ services: depends_on: - mysql - postgres + - clickhouse environment: MYSQL_URL: mysql://root:root@mysql/dbmate POSTGRESQL_URL: postgres://postgres:postgres@postgres/dbmate?sslmode=disable + CLICKHOUSE_URL: clickhouse://clickhouse:9000?database=dbmate mysql: image: mysql:5.7 @@ -20,3 +22,6 @@ services: image: postgres:10 environment: POSTGRES_PASSWORD: postgres + + clickhouse: + image: yandex/clickhouse-server:19.16 diff --git a/go.mod b/go.mod index d9493c6..a67ca27 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/amacneil/dbmate go 1.14 require ( + github.com/ClickHouse/clickhouse-go v1.4.1 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-sql-driver/mysql v1.5.0 diff --git a/go.sum b/go.sum index 9719e7f..19df7c5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,20 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ClickHouse/clickhouse-go v1.4.1 h1:D9cihLg76O1ZyILLaXq1eksYzEuV010NdvucgKGGK14= +github.com/ClickHouse/clickhouse-go v1.4.1/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= +github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0= @@ -16,10 +24,14 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw= github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -27,6 +39,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= diff --git a/pkg/dbmate/clickhouse.go b/pkg/dbmate/clickhouse.go new file mode 100644 index 0000000..35a0901 --- /dev/null +++ b/pkg/dbmate/clickhouse.go @@ -0,0 +1,289 @@ +package dbmate + +import ( + "bytes" + "database/sql" + "fmt" + "net/url" + "regexp" + "sort" + "strings" + + "github.com/ClickHouse/clickhouse-go" +) + +func init() { + RegisterDriver(ClickHouseDriver{}, "clickhouse") +} + +// ClickHouseDriver provides top level database functions +type ClickHouseDriver struct { +} + +func normalizeClickHouseURL(initialURL *url.URL) *url.URL { + u := *initialURL + + u.Scheme = "tcp" + host := u.Host + if u.Port() == "" { + host = fmt.Sprintf("%s:9000", host) + } + u.Host = host + + query := u.Query() + if query.Get("username") == "" && u.User.Username() != "" { + query.Set("username", u.User.Username()) + } + password, passwordSet := u.User.Password() + if query.Get("password") == "" && passwordSet { + query.Set("password", password) + } + u.User = nil + + if query.Get("database") == "" { + path := strings.Trim(u.Path, "/") + if path != "" { + query.Set("database", path) + u.Path = "" + } + } + u.RawQuery = query.Encode() + + return &u +} + +// Open creates a new database connection +func (drv ClickHouseDriver) Open(u *url.URL) (*sql.DB, error) { + return sql.Open("clickhouse", normalizeClickHouseURL(u).String()) +} + +func (drv ClickHouseDriver) openClickHouseDB(u *url.URL) (*sql.DB, error) { + // connect to clickhouse database + clickhouseURL := normalizeClickHouseURL(u) + values := clickhouseURL.Query() + values.Set("database", "default") + clickhouseURL.RawQuery = values.Encode() + + return drv.Open(clickhouseURL) +} + +func (drv ClickHouseDriver) databaseName(u *url.URL) string { + name := normalizeClickHouseURL(u).Query().Get("database") + if name == "" { + name = "default" + } + return name +} + +var clickhouseValidIdentifier = regexp.MustCompile(`^[a-zA-Z_][0-9a-zA-Z_]*$`) + +func clickhouseQuoteIdentifier(str string) string { + if clickhouseValidIdentifier.MatchString(str) { + return str + } + + str = strings.Replace(str, `"`, `""`, -1) + + return fmt.Sprintf(`"%s"`, str) +} + +// CreateDatabase creates the specified database +func (drv ClickHouseDriver) CreateDatabase(u *url.URL) error { + name := drv.databaseName(u) + fmt.Printf("Creating: %s\n", name) + + db, err := drv.openClickHouseDB(u) + if err != nil { + return err + } + defer mustClose(db) + + _, err = db.Exec("create database " + clickhouseQuoteIdentifier(name)) + + return err +} + +// DropDatabase drops the specified database (if it exists) +func (drv ClickHouseDriver) DropDatabase(u *url.URL) error { + name := drv.databaseName(u) + fmt.Printf("Dropping: %s\n", name) + + db, err := drv.openClickHouseDB(u) + if err != nil { + return err + } + defer mustClose(db) + + _, err = db.Exec("drop database if exists " + clickhouseQuoteIdentifier(name)) + + return err +} + +func clickhouseSchemaDump(db *sql.DB, buf *bytes.Buffer, databaseName string) error { + buf.WriteString("\n--\n-- Database schema\n--\n\n") + + buf.WriteString("CREATE DATABASE " + clickhouseQuoteIdentifier(databaseName) + " IF NOT EXISTS;\n\n") + + tables, err := queryColumn(db, "show tables") + if err != nil { + return err + } + sort.Strings(tables) + + for _, table := range tables { + var clause string + err = db.QueryRow("show create table " + clickhouseQuoteIdentifier(table)).Scan(&clause) + if err != nil { + return err + } + buf.WriteString(clause + ";\n\n") + } + return nil +} + +func clickhouseSchemaMigrationsDump(db *sql.DB, buf *bytes.Buffer) error { + // load applied migrations + migrations, err := queryColumn(db, + "select version from schema_migrations final where applied order by version asc", + ) + if err != nil { + return err + } + + quoter := strings.NewReplacer(`\`, `\\`, `'`, `\'`) + for i := range migrations { + migrations[i] = "'" + quoter.Replace(migrations[i]) + "'" + } + + // build schema_migrations table data + buf.WriteString("\n--\n-- Dbmate schema migrations\n--\n\n") + + if len(migrations) > 0 { + buf.WriteString("INSERT INTO schema_migrations (version) VALUES\n (" + + strings.Join(migrations, "),\n (") + + ");\n") + } + + return nil +} + +// DumpSchema returns the current database schema +func (drv ClickHouseDriver) DumpSchema(u *url.URL, db *sql.DB) ([]byte, error) { + var buf bytes.Buffer + var err error + + err = clickhouseSchemaDump(db, &buf, drv.databaseName(u)) + if err != nil { + return nil, err + } + + err = clickhouseSchemaMigrationsDump(db, &buf) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// DatabaseExists determines whether the database exists +func (drv ClickHouseDriver) DatabaseExists(u *url.URL) (bool, error) { + name := drv.databaseName(u) + + db, err := drv.openClickHouseDB(u) + if err != nil { + return false, err + } + defer mustClose(db) + + exists := false + err = db.QueryRow("SELECT 1 FROM system.databases where name = ?", name). + Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + + return exists, err +} + +// CreateMigrationsTable creates the schema_migrations table +func (drv ClickHouseDriver) CreateMigrationsTable(db *sql.DB) error { + _, err := db.Exec(` + create table if not exists schema_migrations ( + version String, + ts DateTime default now(), + applied UInt8 default 1 + ) engine = ReplacingMergeTree(ts) + primary key version + order by version + `) + return err +} + +// SelectMigrations returns a list of applied migrations +// with an optional limit (in descending order) +func (drv ClickHouseDriver) SelectMigrations(db *sql.DB, limit int) (map[string]bool, error) { + query := "select version from schema_migrations final where applied order by version desc" + if limit >= 0 { + query = fmt.Sprintf("%s limit %d", query, limit) + } + rows, err := db.Query(query) + if err != nil { + return nil, err + } + + defer mustClose(rows) + + migrations := map[string]bool{} + for rows.Next() { + var version string + if err := rows.Scan(&version); err != nil { + return nil, err + } + + migrations[version] = true + } + + return migrations, nil +} + +// InsertMigration adds a new migration record +func (drv ClickHouseDriver) InsertMigration(db Transaction, version string) error { + _, err := db.Exec("insert into schema_migrations (version) values (?)", version) + return err +} + +// DeleteMigration removes a migration record +func (drv ClickHouseDriver) DeleteMigration(db Transaction, version string) error { + _, err := db.Exec( + "insert into schema_migrations (version, applied) values (?, ?)", + version, false, + ) + + return err +} + +// Ping verifies a connection to the database server. It does not verify whether the +// specified database exists. +func (drv ClickHouseDriver) Ping(u *url.URL) error { + // attempt connection to primary database, not "clickhouse" database + // to support servers with no "clickhouse" database + // (see https://github.com/amacneil/dbmate/issues/78) + db, err := drv.Open(u) + if err != nil { + return err + } + defer mustClose(db) + + err = db.Ping() + if err == nil { + return nil + } + + // ignore 'Database foo doesn't exist' error + chErr, ok := err.(*clickhouse.Exception) + if ok && chErr.Code == 81 { + return nil + } + + return err +} diff --git a/pkg/dbmate/clickhouse_test.go b/pkg/dbmate/clickhouse_test.go new file mode 100644 index 0000000..48285aa --- /dev/null +++ b/pkg/dbmate/clickhouse_test.go @@ -0,0 +1,305 @@ +package dbmate + +import ( + "database/sql" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func clickhouseTestURL(t *testing.T) *url.URL { + u, err := url.Parse("clickhouse://clickhouse:9000?database=dbmate") + require.NoError(t, err) + + return u +} + +func prepTestClickHouseDB(t *testing.T) *sql.DB { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + + // drop any existing database + err := drv.DropDatabase(u) + require.NoError(t, err) + + // create database + err = drv.CreateDatabase(u) + require.NoError(t, err) + + // connect database + db, err := sql.Open("clickhouse", u.String()) + require.NoError(t, err) + + return db +} + +func TestNormalizeClickHouseURLSimplified(t *testing.T) { + u, err := url.Parse("clickhouse://user:pass@host/db") + require.NoError(t, err) + + s := normalizeClickHouseURL(u).String() + require.Equal(t, "tcp://host:9000?database=db&password=pass&username=user", s) +} + +func TestNormalizeClickHouseURLCanonical(t *testing.T) { + u, err := url.Parse("clickhouse://host:9000?database=db&password=pass&username=user") + require.NoError(t, err) + + s := normalizeClickHouseURL(u).String() + require.Equal(t, "tcp://host:9000?database=db&password=pass&username=user", s) +} + +func TestClickHouseCreateDropDatabase(t *testing.T) { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + + // drop any existing database + err := drv.DropDatabase(u) + require.NoError(t, err) + + // create database + err = drv.CreateDatabase(u) + require.NoError(t, err) + + // check that database exists and we can connect to it + func() { + db, err := sql.Open("clickhouse", u.String()) + require.NoError(t, err) + defer mustClose(db) + + err = db.Ping() + require.NoError(t, err) + }() + + // drop the database + err = drv.DropDatabase(u) + require.NoError(t, err) + + // check that database no longer exists + func() { + db, err := sql.Open("clickhouse", u.String()) + require.NoError(t, err) + defer mustClose(db) + + err = db.Ping() + require.EqualError(t, err, "code: 81, message: Database dbmate doesn't exist") + }() +} + +func TestClickHouseDumpSchema(t *testing.T) { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + + // prepare database + db := prepTestClickHouseDB(t) + defer mustClose(db) + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // insert migration + tx, err := db.Begin() + require.NoError(t, err) + err = drv.InsertMigration(tx, "abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + tx, err = db.Begin() + require.NoError(t, err) + err = drv.InsertMigration(tx, "abc2") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + // DumpSchema should return schema + schema, err := drv.DumpSchema(u, db) + require.NoError(t, err) + require.Contains(t, string(schema), "CREATE TABLE "+drv.databaseName(u)+".schema_migrations") + require.Contains(t, string(schema), "--\n"+ + "-- Dbmate schema migrations\n"+ + "--\n\n"+ + "INSERT INTO schema_migrations (version) VALUES\n"+ + " ('abc1'),\n"+ + " ('abc2');\n") + + // DumpSchema should return error if command fails + values := u.Query() + values.Set("database", "fakedb") + u.RawQuery = values.Encode() + db, err = sql.Open("clickhouse", u.String()) + require.NoError(t, err) + + schema, err = drv.DumpSchema(u, db) + require.Nil(t, schema) + require.EqualError(t, err, "code: 81, message: Database fakedb doesn't exist") +} + +func TestClickHouseDatabaseExists(t *testing.T) { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + + // drop any existing database + err := drv.DropDatabase(u) + require.NoError(t, err) + + // DatabaseExists should return false + exists, err := drv.DatabaseExists(u) + require.NoError(t, err) + require.Equal(t, false, exists) + + // create database + err = drv.CreateDatabase(u) + require.NoError(t, err) + + // DatabaseExists should return true + exists, err = drv.DatabaseExists(u) + require.NoError(t, err) + require.Equal(t, true, exists) +} + +func TestClickHouseDatabaseExists_Error(t *testing.T) { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + values := u.Query() + values.Set("username", "invalid") + u.RawQuery = values.Encode() + + exists, err := drv.DatabaseExists(u) + require.EqualError(t, err, "code: 192, message: Unknown user invalid") + require.Equal(t, false, exists) +} + +func TestClickHouseCreateMigrationsTable(t *testing.T) { + drv := ClickHouseDriver{} + db := prepTestClickHouseDB(t) + defer mustClose(db) + + // migrations table should not exist + count := 0 + err := db.QueryRow("select count(*) from schema_migrations").Scan(&count) + require.EqualError(t, err, "code: 60, message: Table dbmate.schema_migrations doesn't exist.") + + // create table + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // migrations table should exist + err = db.QueryRow("select count(*) from schema_migrations").Scan(&count) + require.NoError(t, err) + + // create table should be idempotent + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) +} + +func TestClickHouseSelectMigrations(t *testing.T) { + drv := ClickHouseDriver{} + db := prepTestClickHouseDB(t) + defer mustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + tx, err := db.Begin() + require.NoError(t, err) + stmt, err := tx.Prepare("insert into schema_migrations (version) values (?)") + require.NoError(t, err) + _, err = stmt.Exec("abc2") + require.NoError(t, err) + _, err = stmt.Exec("abc1") + require.NoError(t, err) + _, err = stmt.Exec("abc3") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + migrations, err := drv.SelectMigrations(db, -1) + require.NoError(t, err) + require.Equal(t, true, migrations["abc1"]) + require.Equal(t, true, migrations["abc2"]) + require.Equal(t, true, migrations["abc2"]) + + // test limit param + migrations, err = drv.SelectMigrations(db, 1) + require.NoError(t, err) + require.Equal(t, true, migrations["abc3"]) + require.Equal(t, false, migrations["abc1"]) + require.Equal(t, false, migrations["abc2"]) +} + +func TestClickHouseInsertMigration(t *testing.T) { + drv := ClickHouseDriver{} + db := prepTestClickHouseDB(t) + defer mustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + count := 0 + err = db.QueryRow("select count(*) from schema_migrations").Scan(&count) + require.NoError(t, err) + require.Equal(t, 0, count) + + // insert migration + tx, err := db.Begin() + require.NoError(t, err) + err = drv.InsertMigration(tx, "abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + err = db.QueryRow("select count(*) from schema_migrations where version = 'abc1'").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) +} + +func TestClickHouseDeleteMigration(t *testing.T) { + drv := ClickHouseDriver{} + db := prepTestClickHouseDB(t) + defer mustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + tx, err := db.Begin() + require.NoError(t, err) + stmt, err := tx.Prepare("insert into schema_migrations (version) values (?)") + require.NoError(t, err) + _, err = stmt.Exec("abc2") + require.NoError(t, err) + _, err = stmt.Exec("abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + tx, err = db.Begin() + require.NoError(t, err) + err = drv.DeleteMigration(tx, "abc2") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + count := 0 + err = db.QueryRow("select count(*) from schema_migrations final where applied").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) +} + +func TestClickHousePing(t *testing.T) { + drv := ClickHouseDriver{} + u := clickhouseTestURL(t) + + // drop any existing database + err := drv.DropDatabase(u) + require.NoError(t, err) + + // ping database + err = drv.Ping(u) + require.NoError(t, err) + + // ping invalid host should return error + u.Host = "clickhouse:404" + err = drv.Ping(u) + require.Error(t, err) + require.Contains(t, err.Error(), "connect: connection refused") +}