Add ClickHouse support (#140)

This commit is contained in:
Ilia Ablamonov 2020-08-08 00:38:48 +04:00 committed by GitHub
parent 8234882546
commit c2dd1bd5af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 633 additions and 4 deletions

View file

@ -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:

View file

@ -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.

View file

@ -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

1
go.mod
View file

@ -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

13
go.sum
View file

@ -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=

289
pkg/dbmate/clickhouse.go Normal file
View file

@ -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
}

View file

@ -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")
}