Repository: amacneil/dbmate
Pull request #432 (2023-04-11 14:31:30)

ClickHouse: Support migrations on cluster
Adding functionally to support usage of Dbmate with ClickHouse cluster.

Easy default usage with just supplying "on_cluster" query parameter.

Took the approach of trying to keep the changes simple without breaking any existing stuff.
95% of the changes in the PR are for the tests and setups of the ClickHouse cluster dockers.

Will happily answer any questions, and would really appreciate reviews.

Solves existing requests in discussions:
https://github.com/amacneil/dbmate/discussions/356
https://github.com/amacneil/dbmate/discussions/244




Patch:
diff --git a/.gitignore b/.gitignore
index 0983582..a653472 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@
 /vendor
 dist
 node_modules
+.vscode
diff --git a/Dockerfile b/Dockerfile
index 76b7b0e..85a6a40 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@ RUN apt-get update \
 
 # golangci-lint
 RUN curl -fsSL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
-	| sh -s -- -b /usr/local/bin v1.51.2
+	| sh -s -- -b /usr/local/bin v1.52.2
 
 # download modules
 COPY go.* /src/
diff --git a/README.md b/README.md
index c86a48a..9c768ee 100644
--- a/README.md
+++ b/README.md
@@ -253,6 +253,33 @@ DATABASE_URL="sqlite:/tmp/database.sqlite3"
 DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name"
 ```
 
+To work with ClickHouse cluster, there are 4 connection query parameters that can be supplied:
+
+- `on_cluster` - Indicataion to use cluster statements and replicated migration table. (default: `false`) If this parameter is not supplied, other cluster related query parameters are ignored.
+```sh
+DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster"
+
+DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster=true"
+```
+
+- `cluster_macro` (Optional) - Macro value to be used for ON CLUSTER statements and for the replciated migration table engine zookeeper path. (default: `{cluster}`)
+
+```sh
+DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&cluster_macro={my_cluster}"
+```
+
+- `replica_macro` (Optional) - Macro value to be used for the replica name in the replciated migration table engine. (default: `{replica}`)
+
+```sh
+DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&replica_macro={my_replica}"
+```
+
+- `zoo_path` (Optional) - The path to the table migration in ClickHouse/Zoo Keeper. (default: `/clickhouse/tables/<cluster_macro>/{table}`)
+
+```sh
+DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&zoo_path=/zk/path/tables"
+```
+
 [See other supported connection options](https://github.com/ClickHouse/clickhouse-go#dsn).
 
 ### Creating Migrations
@@ -521,7 +548,7 @@ Why another database schema migration tool? Dbmate was inspired by many other to
 | Ability to wait for database to become ready                 |              :white_check_mark:              |                                           |                                                      |                                                             |                                                                             |                                                                          |                                 |
 | Database connection string loaded from environment variables |              :white_check_mark:              |                                           |                                                      |                                                             |                                                                             |                                                                          |       :white_check_mark:        |
 | Automatically load .env file                                 |              :white_check_mark:              |                                           |                                                      |                                                             |                                                                             |                                                                          |                                 |
-| No separate configuration file                               |              :white_check_mark:              |                                           |                                                      |                     :white_check_mark:                      |                             :white_check_mark:                              |                            :white_check_mark:                            |                                 |
+| No separate configuration file                               |              :white_check_mark:              |              :white_check_mark:           |                                                      |                     :white_check_mark:                      |                             :white_check_mark:                              |                            :white_check_mark:                            |                                 |
 | Language/framework independent                               |              :white_check_mark:              |            :white_check_mark:             |                                                      |                     :white_check_mark:                      |                                                                             |                                                                          |       :white_check_mark:        |      :white_check_mark:       |
 | **Drivers**                                                  |
 | PostgreSQL                                                   |              :white_check_mark:              |            :white_check_mark:             |                  :white_check_mark:                  |                     :white_check_mark:                      |                             :white_check_mark:                              |                            :white_check_mark:                            |       :white_check_mark:        |      :white_check_mark:       |
diff --git a/RELEASING.md b/RELEASING.md
index ab49b26..2d2e84c 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -4,4 +4,4 @@ The following steps should be followed to publish a new version of dbmate (requi
 
 1. Update [version.go](/pkg/dbmate/version.go) with new version number ([example PR](https://github.com/amacneil/dbmate/pull/146/files))
 2. Create new release on [releases page](https://github.com/amacneil/dbmate/releases) and write release notes
-3. GitHub Actions will automatically publish release binaries and submit Homebrew PR
+3. GitHub Actions will do the rest (publish binaries, NPM package, and Homebrew PR)
diff --git a/docker-compose.yml b/docker-compose.yml
index ef80af8..70396bb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,8 +10,12 @@ services:
       - mysql
       - postgres
       - clickhouse
+      - clickhouse-cluster-01
+      - clickhouse-cluster-02
     environment:
       CLICKHOUSE_TEST_URL: clickhouse://clickhouse:9000/dbmate_test
+      CLICKHOUSE_CLUSTER_01_TEST_URL: clickhouse://ch-cluster-01:9000/dbmate_test
+      CLICKHOUSE_CLUSTER_02_TEST_URL: clickhouse://ch-cluster-02:9000/dbmate_test
       MYSQL_TEST_URL: mysql://root:root@mysql/dbmate_test
       POSTGRES_TEST_URL: postgres://postgres:postgres@postgres/dbmate_test?sslmode=disable
       SQLITE_TEST_URL: sqlite3:/tmp/dbmate_test.sqlite3
@@ -35,3 +39,27 @@ services:
 
   clickhouse:
     image: clickhouse/clickhouse-server:22.8
+
+  zookeeper:
+    image: zookeeper
+    hostname: zookeeper
+
+  clickhouse-cluster-01:
+    image: clickhouse/clickhouse-server:22.8
+    hostname: ch-cluster-01
+    environment:
+      - CLICKHOUSE_CONFIG=/etc/clickhouse-server/config.xml
+    depends_on:
+      - zookeeper
+    volumes:
+      - ./pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01:/etc/clickhouse-server
+
+  clickhouse-cluster-02:
+    image: clickhouse/clickhouse-server:22.8
+    hostname: ch-cluster-02
+    environment:
+      - CLICKHOUSE_CONFIG=/etc/clickhouse-server/config.xml
+    depends_on:
+      - zookeeper
+    volumes:
+      - ./pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02:/etc/clickhouse-server
diff --git a/go.mod b/go.mod
index 72ee2a3..864b039 100644
--- a/go.mod
+++ b/go.mod
@@ -3,26 +3,26 @@ module github.com/amacneil/dbmate/v2
 go 1.20
 
 require (
-	github.com/ClickHouse/clickhouse-go/v2 v2.6.4
-	github.com/go-sql-driver/mysql v1.7.0
-	github.com/joho/godotenv v1.4.0
-	github.com/lib/pq v1.10.7
+	github.com/ClickHouse/clickhouse-go/v2 v2.9.2
+	github.com/go-sql-driver/mysql v1.7.1
+	github.com/joho/godotenv v1.5.1
+	github.com/lib/pq v1.10.9
 	github.com/mattn/go-sqlite3 v1.14.16
-	github.com/stretchr/testify v1.8.1
-	github.com/urfave/cli/v2 v2.24.2
+	github.com/stretchr/testify v1.8.2
+	github.com/urfave/cli/v2 v2.25.3
 	github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04
 )
 
 require (
-	github.com/ClickHouse/ch-go v0.52.1 // indirect
-	github.com/andybalholm/brotli v1.0.4 // indirect
+	github.com/ClickHouse/ch-go v0.55.0 // indirect
+	github.com/andybalholm/brotli v1.0.5 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-faster/city v1.0.1 // indirect
 	github.com/go-faster/errors v0.6.1 // indirect
 	github.com/google/uuid v1.3.0 // indirect
-	github.com/klauspost/compress v1.15.15 // indirect
-	github.com/paulmach/orb v0.8.0 // indirect
+	github.com/klauspost/compress v1.16.5 // indirect
+	github.com/paulmach/orb v0.9.2 // indirect
 	github.com/pierrec/lz4/v4 v4.1.17 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -30,8 +30,8 @@ require (
 	github.com/segmentio/asm v1.2.0 // indirect
 	github.com/shopspring/decimal v1.3.1 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	go.opentelemetry.io/otel v1.13.0 // indirect
-	go.opentelemetry.io/otel/trace v1.13.0 // indirect
-	golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect
+	go.opentelemetry.io/otel v1.15.1 // indirect
+	go.opentelemetry.io/otel/trace v1.15.1 // indirect
+	golang.org/x/sys v0.8.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 2180031..c204efd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,9 @@
-github.com/ClickHouse/ch-go v0.52.1 h1:nucdgfD1BDSHjbNaG3VNebonxJzD8fX8jbuBpfo5VY0=
-github.com/ClickHouse/ch-go v0.52.1/go.mod h1:B9htMJ0hii/zrC2hljUKdnagRBuLqtRG/GrU3jqCwRk=
-github.com/ClickHouse/clickhouse-go/v2 v2.6.4 h1:Rwo7tq2NEHOdYBxXxXnxQrawqIsbQNwZ1nKhYDrn4c0=
-github.com/ClickHouse/clickhouse-go/v2 v2.6.4/go.mod h1:GcNAg9SniIu+BqzOxRsTmXAGvhlSaUm/Y9GFdWUCbX8=
-github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
-github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
+github.com/ClickHouse/ch-go v0.55.0 h1:jw4Tpx887YXrkyL5DfgUome/po8MLz92nz2heOQ6RjQ=
+github.com/ClickHouse/ch-go v0.55.0/go.mod h1:kQT2f+yp2p+sagQA/7kS6G3ukym+GQ5KAu1kuFAFDiU=
+github.com/ClickHouse/clickhouse-go/v2 v2.9.2 h1:P9az39xLJGwdL+Bq04Qrcq0lJspTgGT0VD7ESouFOYg=
+github.com/ClickHouse/clickhouse-go/v2 v2.9.2/go.mod h1:teXfZNM90iQ99Jnuht+dxQXCuhDZ8nvvMoTJOFrcmcg=
+github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
+github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,26 +13,35 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
 github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
 github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
 github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
-github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
-github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
-github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
-github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
-github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
-github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
+github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
-github.com/paulmach/orb v0.8.0 h1:W5XAt5yNPNnhaMNEf0xNSkBMJ1LzOzdk2MRlB6EN0Vs=
-github.com/paulmach/orb v0.8.0/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/paulmach/orb v0.9.2 h1:p/YWV2uJwamAynnDOJGNbPBVtDHj3vG51k9tR1rFwJE=
+github.com/paulmach/orb v0.9.2/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
 github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
 github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
 github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@@ -40,6 +49,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -49,41 +59,57 @@ github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/urfave/cli/v2 v2.24.2 h1:q1VA+ofZ8SWfEKB9xXHUD4QZaeI9e+ItEqSbfH2JBXk=
-github.com/urfave/cli/v2 v2.24.2/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
+github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
 github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
-go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y=
-go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg=
-go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY=
-go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds=
+go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
+go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8=
+go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc=
+go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY=
+go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc=
-golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -94,8 +120,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index 6576384..83b85bf 100644
--- a/main.go
+++ b/main.go
@@ -49,11 +49,11 @@ func NewApp() *cli.App {
 			Value:   "DATABASE_URL",
 			Usage:   "specify an environment variable containing the database URL",
 		},
-		&cli.StringFlag{
+		&cli.StringSliceFlag{
 			Name:    "migrations-dir",
 			Aliases: []string{"d"},
 			EnvVars: []string{"DBMATE_MIGRATIONS_DIR"},
-			Value:   defaultDB.MigrationsDir,
+			Value:   cli.NewStringSlice(defaultDB.MigrationsDir[0]),
 			Usage:   "specify the directory containing migration files",
 		},
 		&cli.StringFlag{
@@ -231,7 +231,7 @@ func action(f func(*dbmate.DB, *cli.Context) error) cli.ActionFunc {
 		}
 		db := dbmate.New(u)
 		db.AutoDumpSchema = !c.Bool("no-dump-schema")
-		db.MigrationsDir = c.String("migrations-dir")
+		db.MigrationsDir = c.StringSlice("migrations-dir")
 		db.MigrationsTableName = c.String("migrations-table")
 		db.SchemaFile = c.String("schema-file")
 		db.WaitBefore = c.Bool("wait")
diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go
index fc404d6..ae598ff 100644
--- a/pkg/dbmate/db.go
+++ b/pkg/dbmate/db.go
@@ -43,8 +43,8 @@ type DB struct {
 	FS fs.FS
 	// Log is the interface to write stdout
 	Log io.Writer
-	// MigrationsDir specifies the directory to find migration files
-	MigrationsDir string
+	// MigrationsDir specifies the directory or directories to find migration files
+	MigrationsDir []string
 	// MigrationsTableName specifies the database table to record migrations in
 	MigrationsTableName string
 	// SchemaFile specifies the location for schema.sql file
@@ -72,7 +72,7 @@ func New(databaseURL *url.URL) *DB {
 		DatabaseURL:         databaseURL,
 		FS:                  nil,
 		Log:                 os.Stdout,
-		MigrationsDir:       "./db/migrations",
+		MigrationsDir:       []string{"./db/migrations"},
 		MigrationsTableName: "schema_migrations",
 		SchemaFile:          "./db/schema.sql",
 		Verbose:             false,
@@ -239,12 +239,12 @@ func (db *DB) NewMigration(name string) error {
 	name = fmt.Sprintf("%s_%s.sql", timestamp, name)
 
 	// create migrations dir if missing
-	if err := ensureDir(db.MigrationsDir); err != nil {
+	if err := ensureDir(db.MigrationsDir[0]); err != nil {
 		return err
 	}
 
 	// check file does not already exist
-	path := filepath.Join(db.MigrationsDir, name)
+	path := filepath.Join(db.MigrationsDir[0], name)
 	fmt.Fprintf(db.Log, "Creating migration: %s\n", path)
 
 	if _, err := os.Stat(path); !os.IsNotExist(err) {
@@ -372,8 +372,8 @@ func (db *DB) printVerbose(result sql.Result) {
 	}
 }
 
-func (db *DB) readMigrationsDir() ([]fs.DirEntry, error) {
-	path := filepath.Clean(db.MigrationsDir)
+func (db *DB) readMigrationsDir(dir string) ([]fs.DirEntry, error) {
+	path := filepath.Clean(dir)
 
 	// We use nil instead of os.DirFS() because DirFS cannot support both relative and absolute
 	// directory paths - it must be anchored at either "." or "/", which we do not know in advance.
@@ -412,35 +412,37 @@ func (db *DB) FindMigrations() ([]Migration, error) {
 		}
 	}
 
-	// find filesystem migrations
-	files, err := db.readMigrationsDir()
-	if err != nil {
-		return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, db.MigrationsDir)
-	}
-
 	migrations := []Migration{}
-	for _, file := range files {
-		if file.IsDir() {
-			continue
+	for _, dir := range db.MigrationsDir {
+		// find filesystem migrations
+		files, err := db.readMigrationsDir(dir)
+		if err != nil {
+			return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, dir)
 		}
 
-		matches := migrationFileRegexp.FindStringSubmatch(file.Name())
-		if len(matches) < 2 {
-			continue
-		}
+		for _, file := range files {
+			if file.IsDir() {
+				continue
+			}
 
-		migration := Migration{
-			Applied:  false,
-			FileName: matches[0],
-			FilePath: filepath.Join(db.MigrationsDir, matches[0]),
-			FS:       db.FS,
-			Version:  matches[1],
-		}
-		if ok := appliedMigrations[migration.Version]; ok {
-			migration.Applied = true
-		}
+			matches := migrationFileRegexp.FindStringSubmatch(file.Name())
+			if len(matches) < 2 {
+				continue
+			}
 
-		migrations = append(migrations, migration)
+			migration := Migration{
+				Applied:  false,
+				FileName: matches[0],
+				FilePath: filepath.Join(dir, matches[0]),
+				FS:       db.FS,
+				Version:  matches[1],
+			}
+			if ok := appliedMigrations[migration.Version]; ok {
+				migration.Applied = true
+			}
+
+			migrations = append(migrations, migration)
+		}
 	}
 
 	sort.Slice(migrations, func(i, j int) bool {
diff --git a/pkg/dbmate/version.go b/pkg/dbmate/version.go
index 1533bf7..a524228 100644
--- a/pkg/dbmate/version.go
+++ b/pkg/dbmate/version.go
@@ -1,4 +1,4 @@
 package dbmate
 
 // Version of dbmate
-const Version = "2.2.0"
+const Version = "2.3.0"
diff --git a/pkg/driver/clickhouse/clickhouse.go b/pkg/driver/clickhouse/clickhouse.go
index 28935d7..0f176f6 100644
--- a/pkg/driver/clickhouse/clickhouse.go
+++ b/pkg/driver/clickhouse/clickhouse.go
@@ -25,6 +25,7 @@ type Driver struct {
 	migrationsTableName string
 	databaseURL         *url.URL
 	log                 io.Writer
+	clusterParameters   *ClusterParameters
 }
 
 // NewDriver initializes the driver
@@ -33,6 +34,7 @@ func NewDriver(config dbmate.DriverConfig) dbmate.Driver {
 		migrationsTableName: config.MigrationsTableName,
 		databaseURL:         config.DatabaseURL,
 		log:                 config.Log,
+		clusterParameters:   ExtractClusterParametersFromURL(config.DatabaseURL),
 	}
 }
 
@@ -74,6 +76,8 @@ func connectionString(initialURL *url.URL) string {
 
 	u.RawQuery = query.Encode()
 
+	u = ClearClusterParametersFromURL(u)
+
 	return u.String()
 }
 
@@ -95,6 +99,15 @@ func (drv *Driver) openClickHouseDB() (*sql.DB, error) {
 	return sql.Open("clickhouse", clickhouseURL.String())
 }
 
+func (drv *Driver) onClusterClause() string {
+	clusterClause := ""
+	if drv.clusterParameters.OnCluster {
+		escapedClusterMacro := drv.escapeString(drv.clusterParameters.ClusterMacro)
+		clusterClause = fmt.Sprintf(" ON CLUSTER '%s'", escapedClusterMacro)
+	}
+	return clusterClause
+}
+
 func (drv *Driver) databaseName() string {
 	name := strings.TrimLeft(dbutil.MustParseURL(connectionString(drv.databaseURL)).Path, "/")
 	if name == "" {
@@ -115,6 +128,12 @@ func (drv *Driver) quoteIdentifier(str string) string {
 	return fmt.Sprintf(`"%s"`, str)
 }
 
+func (drv *Driver) escapeString(str string) string {
+	quoteEscaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`)
+	str = quoteEscaper.Replace(str)
+	return str
+}
+
 // CreateDatabase creates the specified database
 func (drv *Driver) CreateDatabase() error {
 	name := drv.databaseName()
@@ -126,7 +145,9 @@ func (drv *Driver) CreateDatabase() error {
 	}
 	defer dbutil.MustClose(db)
 
-	_, err = db.Exec("create database " + drv.quoteIdentifier(name))
+	q := fmt.Sprintf("CREATE DATABASE %s%s", drv.quoteIdentifier(name), drv.onClusterClause())
+
+	_, err = db.Exec(q)
 
 	return err
 }
@@ -142,14 +163,16 @@ func (drv *Driver) DropDatabase() error {
 	}
 	defer dbutil.MustClose(db)
 
-	_, err = db.Exec("drop database if exists " + drv.quoteIdentifier(name))
+	q := fmt.Sprintf("DROP DATABASE IF EXISTS %s%s", drv.quoteIdentifier(name), drv.onClusterClause())
+
+	_, err = db.Exec(q)
 
 	return err
 }
 
 func (drv *Driver) schemaDump(db *sql.DB, buf *bytes.Buffer, databaseName string) error {
 	buf.WriteString("\n--\n-- Database schema\n--\n\n")
-	buf.WriteString("CREATE DATABASE IF NOT EXISTS " + drv.quoteIdentifier(databaseName) + ";\n\n")
+	buf.WriteString(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s%s;\n\n", drv.quoteIdentifier(databaseName), drv.onClusterClause()))
 
 	tables, err := dbutil.QueryColumn(db, "show tables")
 	if err != nil {
@@ -250,15 +273,22 @@ func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) {
 
 // CreateMigrationsTable creates the schema migrations table
 func (drv *Driver) CreateMigrationsTable(db *sql.DB) error {
+	engineClause := "ReplacingMergeTree(ts)"
+	if drv.clusterParameters.OnCluster {
+		escapedZooPath := drv.escapeString(drv.clusterParameters.ZooPath)
+		escapedReplicaMacro := drv.escapeString(drv.clusterParameters.ReplicaMacro)
+		engineClause = fmt.Sprintf("ReplicatedReplacingMergeTree('%s', '%s', ts)", escapedZooPath, escapedReplicaMacro)
+	}
+
 	_, err := db.Exec(fmt.Sprintf(`
-		create table if not exists %s (
+		create table if not exists %s%s (
 			version String,
 			ts DateTime default now(),
 			applied UInt8 default 1
-		) engine = ReplacingMergeTree(ts)
+		) engine = %s
 		primary key version
 		order by version
-	`, drv.quotedMigrationsTableName()))
+	`, drv.quotedMigrationsTableName(), drv.onClusterClause(), engineClause))
 
 	return err
 }
diff --git a/pkg/driver/clickhouse/cluster_parameters.go b/pkg/driver/clickhouse/cluster_parameters.go
new file mode 100644
index 0000000..3bd3212
--- /dev/null
+++ b/pkg/driver/clickhouse/cluster_parameters.go
@@ -0,0 +1,83 @@
+package clickhouse
+
+import (
+	"fmt"
+	"net/url"
+)
+
+const (
+	OnClusterQueryParam    = "on_cluster"
+	ZooPathQueryParam      = "zoo_path"
+	ClusterMacroQueryParam = "cluster_macro"
+	ReplicaMacroQueryParam = "replica_macro"
+)
+
+type ClusterParameters struct {
+	OnCluster    bool
+	ZooPath      string
+	ClusterMacro string
+	ReplicaMacro string
+}
+
+func ClearClusterParametersFromURL(u *url.URL) *url.URL {
+	q := u.Query()
+	q.Del(OnClusterQueryParam)
+	q.Del(ClusterMacroQueryParam)
+	q.Del(ReplicaMacroQueryParam)
+	q.Del(ZooPathQueryParam)
+	u.RawQuery = q.Encode()
+
+	return u
+}
+
+func ExtractClusterParametersFromURL(u *url.URL) *ClusterParameters {
+	onCluster := extractOnCluster(u)
+	clusterMacro := extractClusterMacro(u)
+	replicaMacro := extractReplicaMacro(u)
+	zookeeperPath := extractZookeeperPath(u)
+
+	r := &ClusterParameters{
+		OnCluster:    onCluster,
+		ZooPath:      zookeeperPath,
+		ClusterMacro: clusterMacro,
+		ReplicaMacro: replicaMacro,
+	}
+
+	return r
+}
+
+func extractOnCluster(u *url.URL) bool {
+	v := u.Query()
+	hasOnCluster := v.Has(OnClusterQueryParam)
+	onClusterValue := v.Get(OnClusterQueryParam)
+	onCluster := hasOnCluster && (onClusterValue == "" || onClusterValue == "true")
+	return onCluster
+}
+
+func extractClusterMacro(u *url.URL) string {
+	v := u.Query()
+	clusterMacro := v.Get(ClusterMacroQueryParam)
+	if clusterMacro == "" {
+		clusterMacro = "{cluster}"
+	}
+	return clusterMacro
+}
+
+func extractReplicaMacro(u *url.URL) string {
+	v := u.Query()
+	replicaMacro := v.Get(ReplicaMacroQueryParam)
+	if replicaMacro == "" {
+		replicaMacro = "{replica}"
+	}
+	return replicaMacro
+}
+
+func extractZookeeperPath(u *url.URL) string {
+	v := u.Query()
+	clusterMacro := extractClusterMacro(u)
+	zookeeperPath := v.Get(ZooPathQueryParam)
+	if zookeeperPath == "" {
+		zookeeperPath = fmt.Sprintf("/clickhouse/tables/%s/{table}", clusterMacro)
+	}
+	return zookeeperPath
+}
diff --git a/typescript/package-lock.json b/typescript/package-lock.json
index 1a17897..5333362 100644
--- a/typescript/package-lock.json
+++ b/typescript/package-lock.json
@@ -21,7 +21,7 @@
         "rimraf": "4.1.2",
         "ts-node": "10.9.1",
         "typescript": "4.9.5",
-        "yaml": "2.2.1"
+        "yaml": "2.2.2"
       }
     },
     "node_modules/@actions/exec": {
@@ -3021,9 +3021,9 @@
       "dev": true
     },
     "node_modules/yaml": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz",
-      "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+      "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
       "dev": true,
       "engines": {
         "node": ">= 14"
@@ -5183,9 +5183,9 @@
       "dev": true
     },
     "yaml": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz",
-      "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz",
+      "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==",
       "dev": true
     },
     "yn": {
diff --git a/typescript/package.json b/typescript/package.json
index 7c3db4f..cd50a9f 100644
--- a/typescript/package.json
+++ b/typescript/package.json
@@ -24,6 +24,6 @@
     "rimraf": "4.1.2",
     "ts-node": "10.9.1",
     "typescript": "4.9.5",
-    "yaml": "2.2.1"
+    "yaml": "2.2.2"
   }
 }


Test patch:
diff --git a/pkg/dbmate/db_test.go b/pkg/dbmate/db_test.go
index 1953ad7..6ba8d5b 100644
--- a/pkg/dbmate/db_test.go
+++ b/pkg/dbmate/db_test.go
@@ -42,7 +42,7 @@ func TestNew(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("foo:test"))
 	require.True(t, db.AutoDumpSchema)
 	require.Equal(t, "foo:test", db.DatabaseURL.String())
-	require.Equal(t, "./db/migrations", db.MigrationsDir)
+	require.Equal(t, []string{"./db/migrations"}, db.MigrationsDir)
 	require.Equal(t, "schema_migrations", db.MigrationsTableName)
 	require.Equal(t, "./db/schema.sql", db.SchemaFile)
 	require.False(t, db.WaitBefore)
@@ -462,7 +462,7 @@ func TestFindMigrationsAbsolute(t *testing.T) {
 	t.Run("relative path", func(t *testing.T) {
 		u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL"))
 		db := newTestDB(t, u)
-		db.MigrationsDir = "db/migrations"
+		db.MigrationsDir = []string{"db/migrations"}
 
 		migrations, err := db.FindMigrations()
 		require.NoError(t, err)
@@ -482,7 +482,7 @@ func TestFindMigrationsAbsolute(t *testing.T) {
 
 		u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL"))
 		db := newTestDB(t, u)
-		db.MigrationsDir = dir
+		db.MigrationsDir = []string{dir}
 		require.Nil(t, db.FS)
 
 		migrations, err := db.FindMigrations()
@@ -550,3 +550,36 @@ drop table users;
 	require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down)
 	require.True(t, parsed.DownOptions.Transaction())
 }
+
+func TestFindMigrationsFSMultipleDirs(t *testing.T) {
+	mapFS := fstest.MapFS{
+		"db/migrations_a/001_test_migration_a.sql": {},
+		"db/migrations_a/005_test_migration_a.sql": {},
+		"db/migrations_b/003_test_migration_b.sql": {},
+		"db/migrations_b/004_test_migration_b.sql": {},
+		"db/migrations_c/002_test_migration_c.sql": {},
+		"db/migrations_c/006_test_migration_c.sql": {},
+	}
+
+	u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL"))
+	db := newTestDB(t, u)
+	db.FS = mapFS
+	db.MigrationsDir = []string{"./db/migrations_a", "./db/migrations_b", "./db/migrations_c"}
+
+	// drop and recreate database
+	err := db.Drop()
+	require.NoError(t, err)
+	err = db.Create()
+	require.NoError(t, err)
+
+	actual, err := db.FindMigrations()
+	require.NoError(t, err)
+
+	// test migrations are correct and in order
+	require.Equal(t, "db/migrations_a/001_test_migration_a.sql", actual[0].FilePath)
+	require.Equal(t, "db/migrations_c/002_test_migration_c.sql", actual[1].FilePath)
+	require.Equal(t, "db/migrations_b/003_test_migration_b.sql", actual[2].FilePath)
+	require.Equal(t, "db/migrations_b/004_test_migration_b.sql", actual[3].FilePath)
+	require.Equal(t, "db/migrations_a/005_test_migration_a.sql", actual[4].FilePath)
+	require.Equal(t, "db/migrations_c/006_test_migration_c.sql", actual[5].FilePath)
+}
diff --git a/pkg/driver/clickhouse/clickhouse_cluster_test.go b/pkg/driver/clickhouse/clickhouse_cluster_test.go
new file mode 100644
index 0000000..0c50a35
--- /dev/null
+++ b/pkg/driver/clickhouse/clickhouse_cluster_test.go
@@ -0,0 +1,351 @@
+package clickhouse
+
+import (
+	"database/sql"
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/amacneil/dbmate/v2/pkg/dbmate"
+	"github.com/amacneil/dbmate/v2/pkg/dbutil"
+
+	"github.com/stretchr/testify/require"
+)
+
+func testClickHouseDriverCluster01(t *testing.T) *Driver {
+	u := fmt.Sprintf("%s?on_cluster", os.Getenv("CLICKHOUSE_CLUSTER_01_TEST_URL"))
+	return testClickHouseDriverURL(t, u)
+}
+
+func testClickHouseDriverCluster02(t *testing.T) *Driver {
+	u := fmt.Sprintf("%s?on_cluster", os.Getenv("CLICKHOUSE_CLUSTER_02_TEST_URL"))
+	return testClickHouseDriverURL(t, u)
+}
+
+func assertDatabaseExists(t *testing.T, drv *Driver, shouldExist bool) {
+	db, err := sql.Open("clickhouse", drv.databaseURL.String())
+	require.NoError(t, err)
+	defer dbutil.MustClose(db)
+
+	err = db.Ping()
+	if shouldExist {
+		require.NoError(t, err)
+	} else {
+		require.EqualError(t, err, "code: 81, message: Database dbmate_test doesn't exist")
+	}
+}
+
+// Makes sure driver creatinon is atomic
+func TestDriverCreationSanity(t *testing.T) {
+	url := fmt.Sprintf("%s?on_cluster", os.Getenv("CLICKHOUSE_CLUSTER_01_TEST_URL"))
+	u := dbutil.MustParseURL(url)
+	dbm := dbmate.New(u)
+	drv, err := dbm.Driver()
+	require.NoError(t, err)
+	drvAgain, err := dbm.Driver()
+	require.NoError(t, err)
+
+	require.Equal(t, drv, drvAgain)
+}
+
+func TestOnClusterClause(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected string
+	}{
+		// not on cluster
+		{"clickhouse://myhost:9000", ""},
+		// on_cluster supplied
+		{"clickhouse://myhost:9000?on_cluster", " ON CLUSTER '{cluster}'"},
+		// on_cluster with supplied macro
+		{"clickhouse://myhost:9000?on_cluster&cluster_macro={cluster2}", " ON CLUSTER '{cluster2}'"},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			drv := testClickHouseDriverURL(t, c.input)
+
+			actual := drv.onClusterClause()
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
+
+func TestClickHouseCreateDropDatabaseOnCluster(t *testing.T) {
+	drv01 := testClickHouseDriverCluster01(t)
+	drv02 := testClickHouseDriverCluster02(t)
+
+	// drop any existing database
+	err := drv01.DropDatabase()
+	require.NoError(t, err)
+
+	// create database
+	err = drv01.CreateDatabase()
+	require.NoError(t, err)
+
+	// check that database exists and we can connect to it
+	assertDatabaseExists(t, drv01, true)
+	// check that database exists on the other clickhouse node and we can connect to it
+	assertDatabaseExists(t, drv02, true)
+
+	// drop the database
+	err = drv01.DropDatabase()
+	require.NoError(t, err)
+
+	// check that database no longer exists
+	assertDatabaseExists(t, drv01, false)
+	// check that database no longer exists on the other clickhouse node
+	assertDatabaseExists(t, drv02, false)
+}
+
+func TestClickHouseDumpSchemaOnCluster(t *testing.T) {
+	drv := testClickHouseDriverCluster01(t)
+	drv.migrationsTableName = "test_migrations"
+
+	// prepare database
+	db := prepTestClickHouseDB(t, drv)
+	defer dbutil.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(db)
+	require.NoError(t, err)
+	require.Contains(t, string(schema), "CREATE TABLE "+drv.databaseName()+".test_migrations")
+	require.Contains(t, string(schema), "ENGINE = ReplicatedReplacingMergeTree")
+	require.Contains(t, string(schema), "--\n"+
+		"-- Dbmate schema migrations\n"+
+		"--\n\n"+
+		"INSERT INTO test_migrations (version) VALUES\n"+
+		"    ('abc1'),\n"+
+		"    ('abc2');\n")
+
+	// DumpSchema should return error if command fails
+	drv.databaseURL.Path = "/fakedb"
+	db, err = sql.Open("clickhouse", drv.databaseURL.String())
+	require.NoError(t, err)
+
+	schema, err = drv.DumpSchema(db)
+	require.Nil(t, schema)
+	require.EqualError(t, err, "code: 81, message: Database fakedb doesn't exist")
+}
+
+func TestClickHouseCreateMigrationsTableOnCluster(t *testing.T) {
+	testCases := []struct {
+		name              string
+		migrationsTable   string
+		expectedTableName string
+	}{
+		{
+			name:              "default table",
+			migrationsTable:   "",
+			expectedTableName: "schema_migrations",
+		},
+		{
+			name:              "custom table",
+			migrationsTable:   "testMigrations",
+			expectedTableName: "\"testMigrations\"",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			drv01 := testClickHouseDriverCluster01(t)
+			drv02 := testClickHouseDriverCluster02(t)
+			if tc.migrationsTable != "" {
+				drv01.migrationsTableName = tc.migrationsTable
+				drv02.migrationsTableName = tc.migrationsTable
+			}
+
+			db01 := prepTestClickHouseDB(t, drv01)
+			defer dbutil.MustClose(db01)
+
+			db02 := prepTestClickHouseDB(t, drv02)
+			defer dbutil.MustClose(db02)
+
+			// migrations table should not exist
+			exists, err := drv01.MigrationsTableExists(db01)
+			require.NoError(t, err)
+			require.Equal(t, false, exists)
+
+			// migrations table should not exist on the other node
+			exists, err = drv02.MigrationsTableExists(db02)
+			require.NoError(t, err)
+			require.Equal(t, false, exists)
+
+			// create table
+			err = drv01.CreateMigrationsTable(db01)
+			require.NoError(t, err)
+
+			// migrations table should exist
+			exists, err = drv01.MigrationsTableExists(db01)
+			require.NoError(t, err)
+			require.Equal(t, true, exists)
+
+			// migrations table should exist on other node
+			exists, err = drv02.MigrationsTableExists(db02)
+			require.NoError(t, err)
+			require.Equal(t, true, exists)
+
+			// create table should be idempotent
+			err = drv01.CreateMigrationsTable(db01)
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestClickHouseSelectMigrationsOnCluster(t *testing.T) {
+	drv01 := testClickHouseDriverCluster01(t)
+	drv02 := testClickHouseDriverCluster02(t)
+	drv01.migrationsTableName = "test_migrations"
+	drv02.migrationsTableName = "test_migrations"
+
+	db01 := prepTestClickHouseDB(t, drv01)
+	defer dbutil.MustClose(db01)
+
+	db02 := prepTestClickHouseDB(t, drv02)
+	defer dbutil.MustClose(db02)
+
+	err := drv01.CreateMigrationsTable(db01)
+	require.NoError(t, err)
+
+	tx, err := db01.Begin()
+	require.NoError(t, err)
+	stmt, err := tx.Prepare("insert into test_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)
+
+	migrations01, err := drv01.SelectMigrations(db01, -1)
+	require.NoError(t, err)
+	require.Equal(t, true, migrations01["abc1"])
+	require.Equal(t, true, migrations01["abc2"])
+	require.Equal(t, true, migrations01["abc3"])
+
+	// Assert select on other node
+	migrations02, err := drv02.SelectMigrations(db02, -1)
+	require.NoError(t, err)
+	require.Equal(t, true, migrations02["abc1"])
+	require.Equal(t, true, migrations02["abc2"])
+	require.Equal(t, true, migrations02["abc3"])
+
+	// test limit param
+	migrations01, err = drv01.SelectMigrations(db01, 1)
+	require.NoError(t, err)
+	require.Equal(t, true, migrations01["abc3"])
+	require.Equal(t, false, migrations01["abc1"])
+	require.Equal(t, false, migrations01["abc2"])
+
+	// test limit param on other node
+	migrations02, err = drv02.SelectMigrations(db02, 1)
+	require.NoError(t, err)
+	require.Equal(t, true, migrations02["abc3"])
+	require.Equal(t, false, migrations02["abc1"])
+	require.Equal(t, false, migrations02["abc2"])
+}
+
+func TestClickHouseInsertMigrationOnCluster(t *testing.T) {
+	drv01 := testClickHouseDriverCluster01(t)
+	drv02 := testClickHouseDriverCluster02(t)
+	drv01.migrationsTableName = "test_migrations"
+	drv02.migrationsTableName = "test_migrations"
+
+	db01 := prepTestClickHouseDB(t, drv01)
+	defer dbutil.MustClose(db01)
+
+	db02 := prepTestClickHouseDB(t, drv02)
+	defer dbutil.MustClose(db02)
+
+	err := drv01.CreateMigrationsTable(db01)
+	require.NoError(t, err)
+
+	count01 := 0
+	err = db01.QueryRow("select count(*) from test_migrations").Scan(&count01)
+	require.NoError(t, err)
+	require.Equal(t, 0, count01)
+
+	count02 := 0
+	err = db02.QueryRow("select count(*) from test_migrations").Scan(&count02)
+	require.NoError(t, err)
+	require.Equal(t, 0, count02)
+
+	// insert migration
+	tx, err := db01.Begin()
+	require.NoError(t, err)
+	err = drv01.InsertMigration(tx, "abc1")
+	require.NoError(t, err)
+	err = tx.Commit()
+	require.NoError(t, err)
+
+	err = db01.QueryRow("select count(*) from test_migrations where version = 'abc1'").Scan(&count01)
+	require.NoError(t, err)
+	require.Equal(t, 1, count01)
+
+	err = db02.QueryRow("select count(*) from test_migrations where version = 'abc1'").Scan(&count02)
+	require.NoError(t, err)
+	require.Equal(t, 1, count02)
+}
+
+func TestClickHouseDeleteMigrationOnCluster(t *testing.T) {
+	drv01 := testClickHouseDriverCluster01(t)
+	drv02 := testClickHouseDriverCluster02(t)
+	drv01.migrationsTableName = "test_migrations"
+	drv02.migrationsTableName = "test_migrations"
+
+	db01 := prepTestClickHouseDB(t, drv01)
+	defer dbutil.MustClose(db01)
+
+	db02 := prepTestClickHouseDB(t, drv02)
+	defer dbutil.MustClose(db02)
+
+	err := drv01.CreateMigrationsTable(db01)
+	require.NoError(t, err)
+
+	tx, err := db01.Begin()
+	require.NoError(t, err)
+	stmt, err := tx.Prepare("insert into test_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 = db01.Begin()
+	require.NoError(t, err)
+	err = drv01.DeleteMigration(tx, "abc2")
+	require.NoError(t, err)
+	err = tx.Commit()
+	require.NoError(t, err)
+
+	count01 := 0
+	err = db01.QueryRow("select count(*) from test_migrations final where applied").Scan(&count01)
+	require.NoError(t, err)
+	require.Equal(t, 1, count01)
+
+	count02 := 0
+	err = db02.QueryRow("select count(*) from test_migrations final where applied").Scan(&count02)
+	require.NoError(t, err)
+	require.Equal(t, 1, count02)
+}
diff --git a/pkg/driver/clickhouse/clickhouse_test.go b/pkg/driver/clickhouse/clickhouse_test.go
index ffd7ee0..ef1f521 100644
--- a/pkg/driver/clickhouse/clickhouse_test.go
+++ b/pkg/driver/clickhouse/clickhouse_test.go
@@ -3,7 +3,6 @@ package clickhouse
 import (
 	"database/sql"
 	"net/url"
-	"os"
 	"testing"
 
 	"github.com/amacneil/dbmate/v2/pkg/dbmate"
@@ -12,32 +11,6 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func testClickHouseDriver(t *testing.T) *Driver {
-	u := dbutil.MustParseURL(os.Getenv("CLICKHOUSE_TEST_URL"))
-	drv, err := dbmate.New(u).Driver()
-	require.NoError(t, err)
-
-	return drv.(*Driver)
-}
-
-func prepTestClickHouseDB(t *testing.T) *sql.DB {
-	drv := testClickHouseDriver(t)
-
-	// drop any existing database
-	err := drv.DropDatabase()
-	require.NoError(t, err)
-
-	// create database
-	err = drv.CreateDatabase()
-	require.NoError(t, err)
-
-	// connect database
-	db, err := sql.Open("clickhouse", drv.databaseURL.String())
-	require.NoError(t, err)
-
-	return db
-}
-
 func TestGetDriver(t *testing.T) {
 	db := dbmate.New(dbutil.MustParseURL("clickhouse://"))
 	drvInterface, err := db.Driver()
@@ -123,7 +96,7 @@ func TestClickHouseDumpSchema(t *testing.T) {
 	drv.migrationsTableName = "test_migrations"
 
 	// prepare database
-	db := prepTestClickHouseDB(t)
+	db := prepTestClickHouseDB(t, drv)
 	defer dbutil.MustClose(db)
 	err := drv.CreateMigrationsTable(db)
 	require.NoError(t, err)
@@ -203,7 +176,7 @@ func TestClickHouseDatabaseExists_Error(t *testing.T) {
 func TestClickHouseCreateMigrationsTable(t *testing.T) {
 	t.Run("default table", func(t *testing.T) {
 		drv := testClickHouseDriver(t)
-		db := prepTestClickHouseDB(t)
+		db := prepTestClickHouseDB(t, drv)
 		defer dbutil.MustClose(db)
 
 		// migrations table should not exist
@@ -242,7 +215,7 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) {
 		drv := testClickHouseDriver(t)
 		drv.migrationsTableName = "testMigrations"
 
-		db := prepTestClickHouseDB(t)
+		db := prepTestClickHouseDB(t, drv)
 		defer dbutil.MustClose(db)
 
 		// migrations table should not exist
@@ -282,7 +255,7 @@ func TestClickHouseSelectMigrations(t *testing.T) {
 	drv := testClickHouseDriver(t)
 	drv.migrationsTableName = "test_migrations"
 
-	db := prepTestClickHouseDB(t)
+	db := prepTestClickHouseDB(t, drv)
 	defer dbutil.MustClose(db)
 
 	err := drv.CreateMigrationsTable(db)
@@ -319,7 +292,7 @@ func TestClickHouseInsertMigration(t *testing.T) {
 	drv := testClickHouseDriver(t)
 	drv.migrationsTableName = "test_migrations"
 
-	db := prepTestClickHouseDB(t)
+	db := prepTestClickHouseDB(t, drv)
 	defer dbutil.MustClose(db)
 
 	err := drv.CreateMigrationsTable(db)
@@ -347,7 +320,7 @@ func TestClickHouseDeleteMigration(t *testing.T) {
 	drv := testClickHouseDriver(t)
 	drv.migrationsTableName = "test_migrations"
 
-	db := prepTestClickHouseDB(t)
+	db := prepTestClickHouseDB(t, drv)
 	defer dbutil.MustClose(db)
 
 	err := drv.CreateMigrationsTable(db)
@@ -418,3 +391,26 @@ func TestClickHouseQuotedMigrationsTableName(t *testing.T) {
 		require.Equal(t, `"bizarre""$name"`, name)
 	})
 }
+
+func TestEscapeString(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected string
+	}{
+		// nothig to escape
+		{`lets go`, `lets go`},
+		// escape '
+		{`let's go`, `let\'s go`},
+		// escape \
+		{`let\s go`, `let\\s go`},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			drv := testClickHouseDriver(t)
+
+			actual := drv.escapeString(c.input)
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
diff --git a/pkg/driver/clickhouse/clickhouse_testutils_test.go b/pkg/driver/clickhouse/clickhouse_testutils_test.go
new file mode 100644
index 0000000..0dfb4cf
--- /dev/null
+++ b/pkg/driver/clickhouse/clickhouse_testutils_test.go
@@ -0,0 +1,40 @@
+package clickhouse
+
+import (
+	"database/sql"
+	"os"
+	"testing"
+
+	"github.com/amacneil/dbmate/v2/pkg/dbmate"
+	"github.com/amacneil/dbmate/v2/pkg/dbutil"
+
+	"github.com/stretchr/testify/require"
+)
+
+func testClickHouseDriverURL(t *testing.T, url string) *Driver {
+	u := dbutil.MustParseURL(url)
+	drv, err := dbmate.New(u).Driver()
+	require.NoError(t, err)
+
+	return drv.(*Driver)
+}
+
+func testClickHouseDriver(t *testing.T) *Driver {
+	return testClickHouseDriverURL(t, os.Getenv("CLICKHOUSE_TEST_URL"))
+}
+
+func prepTestClickHouseDB(t *testing.T, drv *Driver) *sql.DB {
+	// drop any existing database
+	err := drv.DropDatabase()
+	require.NoError(t, err)
+
+	// create database
+	err = drv.CreateDatabase()
+	require.NoError(t, err)
+
+	// connect database
+	db, err := drv.Open()
+	require.NoError(t, err)
+
+	return db
+}
diff --git a/pkg/driver/clickhouse/cluster_parameters_test.go b/pkg/driver/clickhouse/cluster_parameters_test.go
new file mode 100644
index 0000000..fb8c29f
--- /dev/null
+++ b/pkg/driver/clickhouse/cluster_parameters_test.go
@@ -0,0 +1,97 @@
+package clickhouse
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/amacneil/dbmate/v2/pkg/dbutil"
+)
+
+func TestOnCluster(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected bool
+	}{
+		// param not supplied
+		{"clickhouse://myhost:9000", false},
+		// empty on_cluster parameter
+		{"clickhouse://myhost:9000?on_cluster", true},
+		// true on_cluster parameter
+		{"clickhouse://myhost:9000?on_cluster=true", true},
+		// any other value on_cluster parameter
+		{"clickhouse://myhost:9000?on_cluster=falsy", false},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			u := dbutil.MustParseURL(c.input)
+
+			actual := extractOnCluster(u)
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
+
+func TestClusterMacro(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected string
+	}{
+		// cluster_macro not supplied
+		{"clickhouse://myhost:9000", "{cluster}"},
+		// cluster_macro supplied
+		{"clickhouse://myhost:9000?cluster_macro={cluster2}", "{cluster2}"},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			u := dbutil.MustParseURL(c.input)
+
+			actual := extractClusterMacro(u)
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
+
+func TestReplicaMacro(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected string
+	}{
+		// replica_macro not supplied
+		{"clickhouse://myhost:9000", "{replica}"},
+		// replica_macro supplied
+		{"clickhouse://myhost:9000?replica_macro={replica2}", "{replica2}"},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			u := dbutil.MustParseURL(c.input)
+
+			actual := extractReplicaMacro(u)
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
+
+func TestZookeeperPath(t *testing.T) {
+	cases := []struct {
+		input    string
+		expected string
+	}{
+		// zoo_path not supplied
+		{"clickhouse://myhost:9000", "/clickhouse/tables/{cluster}/{table}"},
+		// zoo_path supplied
+		{"clickhouse://myhost:9000?zoo_path=/zk/path/tables", "/zk/path/tables"},
+	}
+
+	for _, c := range cases {
+		t.Run(c.input, func(t *testing.T) {
+			u := dbutil.MustParseURL(c.input)
+
+			actual := extractZookeeperPath(u)
+			require.Equal(t, c.expected, actual)
+		})
+	}
+}
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml
new file mode 100644
index 0000000..f0a4156
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml
@@ -0,0 +1,3 @@
+<clickhouse>
+    <database_atomic_delay_before_drop_table_sec>0</database_atomic_delay_before_drop_table_sec>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml
new file mode 100644
index 0000000..3025dc2
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml
@@ -0,0 +1,12 @@
+<clickhouse>
+     <!-- Listen wildcard address to allow accepting connections from other containers and host network. -->
+    <listen_host>::</listen_host>
+    <listen_host>0.0.0.0</listen_host>
+    <listen_try>1</listen_try>
+
+    <!--
+    <logger>
+        <console>1</console>
+    </logger>
+    -->
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml
new file mode 100644
index 0000000..23a7c8b
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<!--
+  NOTE: User and query level settings are set up in "users.xml" file.
+  If you have accidentally specified user-level settings here, server won't start.
+  You can either move the settings to the right place inside "users.xml" file
+   or add <skip_check_for_incorrect_settings>1</skip_check_for_incorrect_settings> here.
+-->
+<clickhouse>
+    <logger>
+        <level>trace</level>
+        <log>/var/log/clickhouse-server/clickhouse-server.log</log>
+        <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
+        <size>100M</size>
+        <count>2</count>
+    </logger>
+
+    <http_port>8123</http_port>
+    <tcp_port>9000</tcp_port>
+    <interserver_http_port>9009</interserver_http_port>
+
+    <max_connections>4096</max_connections>
+    <keep_alive_timeout>3</keep_alive_timeout>
+    <concurrent_threads_soft_limit>0</concurrent_threads_soft_limit>
+    <max_concurrent_queries>100</max_concurrent_queries>
+    <max_server_memory_usage>0</max_server_memory_usage>
+    <max_thread_pool_size>10000</max_thread_pool_size>
+    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>
+    <total_memory_profiler_step>4194304</total_memory_profiler_step>
+    <total_memory_tracker_sample_probability>0</total_memory_tracker_sample_probability>
+    <uncompressed_cache_size>8589934592</uncompressed_cache_size>
+    <mark_cache_size>5368709120</mark_cache_size>
+    <mmap_cache_size>1000</mmap_cache_size>
+    <compiled_expression_cache_size>134217728</compiled_expression_cache_size>
+    <compiled_expression_cache_elements_size>10000</compiled_expression_cache_elements_size>
+
+    <path>/var/lib/clickhouse/</path>
+    <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
+    <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
+
+    <user_directories>
+        <users_xml>
+            <path>users.xml</path>
+        </users_xml>
+        <local_directory>
+            <path>/var/lib/clickhouse/access/</path>
+        </local_directory>
+    </user_directories>
+
+    <access_control_improvements>
+        <users_without_row_policies_can_read_rows>false</users_without_row_policies_can_read_rows>
+        <on_cluster_queries_require_cluster_grant>false</on_cluster_queries_require_cluster_grant>
+        <select_from_system_db_requires_grant>false</select_from_system_db_requires_grant>
+        <select_from_information_schema_requires_grant>false</select_from_information_schema_requires_grant>
+    </access_control_improvements>
+
+    <default_profile>default</default_profile>
+    <default_database>default</default_database>
+    <mlock_executable>true</mlock_executable>
+
+
+    <!-- Configuration of clusters that could be used in Distributed tables.
+         https://clickhouse.com/docs/en/operations/table_engines/distributed/
+      -->
+    <remote_servers>
+      <cluster-01>
+        <shard>
+            <!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
+            <!-- <internal_replication>false</internal_replication> -->
+            <!-- Optional. Shard weight when writing data. Default: 1. -->
+            <!-- <weight>1</weight> -->
+            <internal_replication>true</internal_replication>
+            <replica>
+                <host>ch-cluster-01</host>
+                <port>9000</port>
+                <!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
+                <!-- <priority>1</priority> -->
+            </replica>
+        </shard>
+        <shard>
+            <internal_replication>true</internal_replication>
+            <replica>
+                <host>ch-cluster-02</host>
+                <port>9000</port>
+            </replica>
+        </shard>
+      </cluster-01>
+    </remote_servers>
+
+    <zookeeper>
+        <node>
+            <host>zookeeper</host>
+            <port>2181</port>
+        </node>
+    </zookeeper>
+
+    <macros>
+        <cluster>cluster-01</cluster>
+        <shard>shard-01</shard>
+        <replica>ch_cluster-01</replica>
+    </macros>
+
+
+    <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>
+    <max_session_timeout>3600</max_session_timeout>
+    <default_session_timeout>60</default_session_timeout>
+
+    <query_log>
+        <database>system</database>
+        <table>query_log</table>
+        <partition_by>toYYYYMM(event_date)</partition_by>
+
+        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
+    </query_log>
+
+    <dictionaries_config>*_dictionary.xml</dictionaries_config>
+
+    <!-- Allow to execute distributed DDL queries (CREATE, DROP, ALTER, RENAME) on cluster.
+         Works only if ZooKeeper is enabled. Comment it if such functionality isn't required. -->
+    <distributed_ddl>
+        <!-- Path in ZooKeeper to queue with DDL queries -->
+        <path>/clickhouse/task_queue/ddl</path>
+
+    </distributed_ddl>
+
+    <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
+
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml
new file mode 100644
index 0000000..386fce0
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml
@@ -0,0 +1,7 @@
+<clickhouse>
+    <profiles>
+        <default>
+            <database_atomic_wait_for_drop_and_detach_synchronously>1</database_atomic_wait_for_drop_and_detach_synchronously>
+        </default>
+    </profiles>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml
new file mode 100644
index 0000000..34cce83
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml
@@ -0,0 +1,10 @@
+<clickhouse>
+    <profiles>
+        <default>
+        <!-- Used for tests only
+             INSERT succeeds only when ClickHouse manages to correctly write data to the insert_quorum of replicas during the insert_quorum_timeout
+             Helps querying data right after inserting it into the replciated table  -->
+            <insert_quorum>2</insert_quorum>
+        </default>
+    </profiles>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml
new file mode 100644
index 0000000..96067d0
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<clickhouse>
+    <!-- See also the files in users.d directory where the settings can be overridden. -->
+
+    <!-- Profiles of settings. -->
+    <profiles>
+        <!-- Default settings. -->
+        <default>
+            <!-- How to choose between replicas during distributed query processing.
+                 random - choose random replica from set of replicas with minimum number of errors
+                 nearest_hostname - from set of replicas with minimum number of errors, choose replica
+                  with minimum number of different symbols between replica's hostname and local hostname
+                  (Hamming distance).
+                 in_order - first live replica is chosen in specified order.
+                 first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
+            -->
+            <load_balancing>random</load_balancing>
+        </default>
+
+        <!-- Profile that allows only read queries. -->
+        <readonly>
+            <readonly>1</readonly>
+        </readonly>
+    </profiles>
+
+    <!-- Users and ACL. -->
+    <users>
+        <!-- If user name was not specified, 'default' user is used. -->
+        <default>
+            <!-- See also the files in users.d directory where the password can be overridden.
+
+                 Password could be specified in plaintext or in SHA256 (in hex format).
+
+                 If you want to specify password in plaintext (not recommended), place it in 'password' element.
+                 Example: <password>qwerty</password>.
+                 Password could be empty.
+
+                 If you want to specify SHA256, place it in 'password_sha256_hex' element.
+                 Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
+                 Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
+
+                 If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
+                 Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
+
+                 If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
+                  place its name in 'server' element inside 'ldap' element.
+                 Example: <ldap><server>my_ldap_server</server></ldap>
+
+                 If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
+                  place 'kerberos' element instead of 'password' (and similar) elements.
+                 The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
+                 You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
+                  whose initiator's realm matches it.
+                 Example: <kerberos />
+                 Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
+
+                 How to generate decent password:
+                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
+                 In first line will be password and in second - corresponding SHA256.
+
+                 How to generate double SHA1:
+                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
+                 In first line will be password and in second - corresponding double SHA1.
+            -->
+            <password></password>
+
+            <!-- List of networks with open access.
+
+                 To open access from everywhere, specify:
+                    <ip>::/0</ip>
+
+                 To open access only from localhost, specify:
+                    <ip>::1</ip>
+                    <ip>127.0.0.1</ip>
+
+                 Each element of list has one of the following forms:
+                 <ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
+                     2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
+                 <host> Hostname. Example: server01.clickhouse.com.
+                     To check access, DNS query is performed, and all received addresses compared to peer address.
+                 <host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
+                     To check access, DNS PTR query is performed for peer address and then regexp is applied.
+                     Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
+                     Strongly recommended that regexp is ends with $
+                 All results of DNS requests are cached till server restart.
+            -->
+            <networks>
+                <ip>::/0</ip>
+            </networks>
+
+            <!-- Settings profile for user. -->
+            <profile>default</profile>
+
+            <!-- Quota for user. -->
+            <quota>default</quota>
+
+            <!-- User can create other users and grant rights to them. -->
+            <!-- <access_management>1</access_management> -->
+        </default>
+    </users>
+
+    <!-- Quotas. -->
+    <quotas>
+        <!-- Name of quota. -->
+        <default>
+            <!-- Limits for time interval. You could specify many intervals with different limits. -->
+            <interval>
+                <!-- Length of interval. -->
+                <duration>3600</duration>
+
+                <!-- No limits. Just calculate resource usage for time interval. -->
+                <queries>0</queries>
+                <errors>0</errors>
+                <result_rows>0</result_rows>
+                <read_rows>0</read_rows>
+                <execution_time>0</execution_time>
+            </interval>
+        </default>
+    </quotas>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml
new file mode 100644
index 0000000..f0a4156
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml
@@ -0,0 +1,3 @@
+<clickhouse>
+    <database_atomic_delay_before_drop_table_sec>0</database_atomic_delay_before_drop_table_sec>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml
new file mode 100644
index 0000000..3025dc2
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml
@@ -0,0 +1,12 @@
+<clickhouse>
+     <!-- Listen wildcard address to allow accepting connections from other containers and host network. -->
+    <listen_host>::</listen_host>
+    <listen_host>0.0.0.0</listen_host>
+    <listen_try>1</listen_try>
+
+    <!--
+    <logger>
+        <console>1</console>
+    </logger>
+    -->
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml
new file mode 100644
index 0000000..b9a1e2c
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0"?>
+<!--
+  NOTE: User and query level settings are set up in "users.xml" file.
+  If you have accidentally specified user-level settings here, server won't start.
+  You can either move the settings to the right place inside "users.xml" file
+   or add <skip_check_for_incorrect_settings>1</skip_check_for_incorrect_settings> here.
+-->
+<clickhouse>
+    <logger>
+        <level>trace</level>
+        <log>/var/log/clickhouse-server/clickhouse-server.log</log>
+        <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
+        <size>100M</size>
+        <count>2</count>
+    </logger>
+
+    <http_port>8123</http_port>
+    <tcp_port>9000</tcp_port>
+    <interserver_http_port>9009</interserver_http_port>
+
+    <max_connections>4096</max_connections>
+    <keep_alive_timeout>3</keep_alive_timeout>
+    <concurrent_threads_soft_limit>0</concurrent_threads_soft_limit>
+    <max_concurrent_queries>100</max_concurrent_queries>
+    <max_server_memory_usage>0</max_server_memory_usage>
+    <max_thread_pool_size>10000</max_thread_pool_size>
+    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>
+    <total_memory_profiler_step>4194304</total_memory_profiler_step>
+    <total_memory_tracker_sample_probability>0</total_memory_tracker_sample_probability>
+    <uncompressed_cache_size>8589934592</uncompressed_cache_size>
+    <mark_cache_size>5368709120</mark_cache_size>
+    <mmap_cache_size>1000</mmap_cache_size>
+    <compiled_expression_cache_size>134217728</compiled_expression_cache_size>
+    <compiled_expression_cache_elements_size>10000</compiled_expression_cache_elements_size>
+
+    <path>/var/lib/clickhouse/</path>
+    <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
+    <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
+
+    <user_directories>
+        <users_xml>
+            <path>users.xml</path>
+        </users_xml>
+        <local_directory>
+            <path>/var/lib/clickhouse/access/</path>
+        </local_directory>
+    </user_directories>
+
+    <access_control_improvements>
+        <users_without_row_policies_can_read_rows>false</users_without_row_policies_can_read_rows>
+        <on_cluster_queries_require_cluster_grant>false</on_cluster_queries_require_cluster_grant>
+        <select_from_system_db_requires_grant>false</select_from_system_db_requires_grant>
+        <select_from_information_schema_requires_grant>false</select_from_information_schema_requires_grant>
+    </access_control_improvements>
+
+    <default_profile>default</default_profile>
+    <default_database>default</default_database>
+    <mlock_executable>true</mlock_executable>
+
+
+    <!-- Configuration of clusters that could be used in Distributed tables.
+         https://clickhouse.com/docs/en/operations/table_engines/distributed/
+      -->
+    <remote_servers>
+      <cluster-01>
+        <shard>
+            <!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
+            <!-- <internal_replication>false</internal_replication> -->
+            <!-- Optional. Shard weight when writing data. Default: 1. -->
+            <!-- <weight>1</weight> -->
+            <internal_replication>true</internal_replication>
+            <replica>
+                <host>ch-cluster-01</host>
+                <port>9000</port>
+                <!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
+                <!-- <priority>1</priority> -->
+            </replica>
+        </shard>
+        <shard>
+            <internal_replication>true</internal_replication>
+            <replica>
+                <host>ch-cluster-02</host>
+                <port>9000</port>
+            </replica>
+        </shard>
+      </cluster-01>
+    </remote_servers>
+
+    <zookeeper>
+        <node>
+            <host>zookeeper</host>
+            <port>2181</port>
+        </node>
+    </zookeeper>
+
+    <macros>
+        <cluster>cluster-01</cluster>
+        <shard>shard-02</shard>
+        <replica>ch_cluster-02</replica>
+    </macros>
+
+
+    <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>
+    <max_session_timeout>3600</max_session_timeout>
+    <default_session_timeout>60</default_session_timeout>
+
+    <query_log>
+        <database>system</database>
+        <table>query_log</table>
+        <partition_by>toYYYYMM(event_date)</partition_by>
+
+        <flush_interval_milliseconds>7500</flush_interval_milliseconds>
+    </query_log>
+
+    <dictionaries_config>*_dictionary.xml</dictionaries_config>
+
+    <!-- Allow to execute distributed DDL queries (CREATE, DROP, ALTER, RENAME) on cluster.
+         Works only if ZooKeeper is enabled. Comment it if such functionality isn't required. -->
+    <distributed_ddl>
+        <!-- Path in ZooKeeper to queue with DDL queries -->
+        <path>/clickhouse/task_queue/ddl</path>
+
+    </distributed_ddl>
+
+    <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
+
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml
new file mode 100644
index 0000000..386fce0
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml
@@ -0,0 +1,7 @@
+<clickhouse>
+    <profiles>
+        <default>
+            <database_atomic_wait_for_drop_and_detach_synchronously>1</database_atomic_wait_for_drop_and_detach_synchronously>
+        </default>
+    </profiles>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml
new file mode 100644
index 0000000..34cce83
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml
@@ -0,0 +1,10 @@
+<clickhouse>
+    <profiles>
+        <default>
+        <!-- Used for tests only
+             INSERT succeeds only when ClickHouse manages to correctly write data to the insert_quorum of replicas during the insert_quorum_timeout
+             Helps querying data right after inserting it into the replciated table  -->
+            <insert_quorum>2</insert_quorum>
+        </default>
+    </profiles>
+</clickhouse>
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml
new file mode 100644
index 0000000..96067d0
--- /dev/null
+++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<clickhouse>
+    <!-- See also the files in users.d directory where the settings can be overridden. -->
+
+    <!-- Profiles of settings. -->
+    <profiles>
+        <!-- Default settings. -->
+        <default>
+            <!-- How to choose between replicas during distributed query processing.
+                 random - choose random replica from set of replicas with minimum number of errors
+                 nearest_hostname - from set of replicas with minimum number of errors, choose replica
+                  with minimum number of different symbols between replica's hostname and local hostname
+                  (Hamming distance).
+                 in_order - first live replica is chosen in specified order.
+                 first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
+            -->
+            <load_balancing>random</load_balancing>
+        </default>
+
+        <!-- Profile that allows only read queries. -->
+        <readonly>
+            <readonly>1</readonly>
+        </readonly>
+    </profiles>
+
+    <!-- Users and ACL. -->
+    <users>
+        <!-- If user name was not specified, 'default' user is used. -->
+        <default>
+            <!-- See also the files in users.d directory where the password can be overridden.
+
+                 Password could be specified in plaintext or in SHA256 (in hex format).
+
+                 If you want to specify password in plaintext (not recommended), place it in 'password' element.
+                 Example: <password>qwerty</password>.
+                 Password could be empty.
+
+                 If you want to specify SHA256, place it in 'password_sha256_hex' element.
+                 Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
+                 Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
+
+                 If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
+                 Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
+
+                 If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
+                  place its name in 'server' element inside 'ldap' element.
+                 Example: <ldap><server>my_ldap_server</server></ldap>
+
+                 If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
+                  place 'kerberos' element instead of 'password' (and similar) elements.
+                 The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
+                 You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
+                  whose initiator's realm matches it.
+                 Example: <kerberos />
+                 Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
+
+                 How to generate decent password:
+                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
+                 In first line will be password and in second - corresponding SHA256.
+
+                 How to generate double SHA1:
+                 Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
+                 In first line will be password and in second - corresponding double SHA1.
+            -->
+            <password></password>
+
+            <!-- List of networks with open access.
+
+                 To open access from everywhere, specify:
+                    <ip>::/0</ip>
+
+                 To open access only from localhost, specify:
+                    <ip>::1</ip>
+                    <ip>127.0.0.1</ip>
+
+                 Each element of list has one of the following forms:
+                 <ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
+                     2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
+                 <host> Hostname. Example: server01.clickhouse.com.
+                     To check access, DNS query is performed, and all received addresses compared to peer address.
+                 <host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
+                     To check access, DNS PTR query is performed for peer address and then regexp is applied.
+                     Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
+                     Strongly recommended that regexp is ends with $
+                 All results of DNS requests are cached till server restart.
+            -->
+            <networks>
+                <ip>::/0</ip>
+            </networks>
+
+            <!-- Settings profile for user. -->
+            <profile>default</profile>
+
+            <!-- Quota for user. -->
+            <quota>default</quota>
+
+            <!-- User can create other users and grant rights to them. -->
+            <!-- <access_management>1</access_management> -->
+        </default>
+    </users>
+
+    <!-- Quotas. -->
+    <quotas>
+        <!-- Name of quota. -->
+        <default>
+            <!-- Limits for time interval. You could specify many intervals with different limits. -->
+            <interval>
+                <!-- Length of interval. -->
+                <duration>3600</duration>
+
+                <!-- No limits. Just calculate resource usage for time interval. -->
+                <queries>0</queries>
+                <errors>0</errors>
+                <result_rows>0</result_rows>
+                <read_rows>0</read_rows>
+                <execution_time>0</execution_time>
+            </interval>
+        </default>
+    </quotas>
+</clickhouse>