github.com/c0olix/migrate@v3.5.4+incompatible/database/clickhouse/clickhouse.go (about) 1 package clickhouse 2 3 import ( 4 "database/sql" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/url" 9 "strings" 10 "time" 11 12 "github.com/golang-migrate/migrate" 13 "github.com/golang-migrate/migrate/database" 14 ) 15 16 var DefaultMigrationsTable = "schema_migrations" 17 18 var ErrNilConfig = fmt.Errorf("no config") 19 20 type Config struct { 21 DatabaseName string 22 MigrationsTable string 23 MultiStatementEnabled bool 24 } 25 26 func init() { 27 database.Register("clickhouse", &ClickHouse{}) 28 } 29 30 func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) { 31 if config == nil { 32 return nil, ErrNilConfig 33 } 34 35 if err := conn.Ping(); err != nil { 36 return nil, err 37 } 38 39 ch := &ClickHouse{ 40 conn: conn, 41 config: config, 42 } 43 44 if err := ch.init(); err != nil { 45 return nil, err 46 } 47 48 return ch, nil 49 } 50 51 type ClickHouse struct { 52 conn *sql.DB 53 config *Config 54 } 55 56 func (ch *ClickHouse) Open(dsn string) (database.Driver, error) { 57 purl, err := url.Parse(dsn) 58 if err != nil { 59 return nil, err 60 } 61 q := migrate.FilterCustomQuery(purl) 62 q.Scheme = "tcp" 63 conn, err := sql.Open("clickhouse", q.String()) 64 if err != nil { 65 return nil, err 66 } 67 68 ch = &ClickHouse{ 69 conn: conn, 70 config: &Config{ 71 MigrationsTable: purl.Query().Get("x-migrations-table"), 72 DatabaseName: purl.Query().Get("database"), 73 MultiStatementEnabled: purl.Query().Get("x-multi-statement") == "true", 74 }, 75 } 76 77 if err := ch.init(); err != nil { 78 return nil, err 79 } 80 81 return ch, nil 82 } 83 84 func (ch *ClickHouse) init() error { 85 if len(ch.config.DatabaseName) == 0 { 86 if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil { 87 return err 88 } 89 } 90 91 if len(ch.config.MigrationsTable) == 0 { 92 ch.config.MigrationsTable = DefaultMigrationsTable 93 } 94 95 return ch.ensureVersionTable() 96 } 97 98 func (ch *ClickHouse) Run(r io.Reader) error { 99 migration, err := ioutil.ReadAll(r) 100 if err != nil { 101 return err 102 } 103 104 if ch.config.MultiStatementEnabled { 105 // split query by semi-colon 106 queries := strings.Split(string(migration), ";") 107 for _, q := range queries { 108 tq := strings.TrimSpace(q) 109 if tq == "" { 110 continue 111 } 112 if _, err := ch.conn.Exec(string(q)); err != nil { 113 return database.Error{OrigErr: err, Err: "migration failed", Query: []byte(q)} 114 } 115 } 116 return nil 117 } 118 119 if _, err := ch.conn.Exec(string(migration)); err != nil { 120 return database.Error{OrigErr: err, Err: "migration failed", Query: migration} 121 } 122 123 return nil 124 } 125 func (ch *ClickHouse) Version() (int, bool, error) { 126 var ( 127 version int 128 dirty uint8 129 query = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1" 130 ) 131 if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil { 132 if err == sql.ErrNoRows { 133 return database.NilVersion, false, nil 134 } 135 return 0, false, &database.Error{OrigErr: err, Query: []byte(query)} 136 } 137 return version, dirty == 1, nil 138 } 139 140 func (ch *ClickHouse) SetVersion(version int, dirty bool) error { 141 var ( 142 bool = func(v bool) uint8 { 143 if v { 144 return 1 145 } 146 return 0 147 } 148 tx, err = ch.conn.Begin() 149 ) 150 if err != nil { 151 return err 152 } 153 154 query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)" 155 if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil { 156 return &database.Error{OrigErr: err, Query: []byte(query)} 157 } 158 159 return tx.Commit() 160 } 161 162 func (ch *ClickHouse) ensureVersionTable() error { 163 var ( 164 table string 165 query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'" 166 ) 167 // check if migration table exists 168 if err := ch.conn.QueryRow(query).Scan(&table); err != nil { 169 if err != sql.ErrNoRows { 170 return &database.Error{OrigErr: err, Query: []byte(query)} 171 } 172 } else { 173 return nil 174 } 175 // if not, create the empty migration table 176 query = ` 177 CREATE TABLE ` + ch.config.MigrationsTable + ` ( 178 version UInt32, 179 dirty UInt8, 180 sequence UInt64 181 ) Engine=TinyLog 182 ` 183 if _, err := ch.conn.Exec(query); err != nil { 184 return &database.Error{OrigErr: err, Query: []byte(query)} 185 } 186 return nil 187 } 188 189 func (ch *ClickHouse) Drop() error { 190 var ( 191 query = "SHOW TABLES FROM " + ch.config.DatabaseName 192 tables, err = ch.conn.Query(query) 193 ) 194 if err != nil { 195 return &database.Error{OrigErr: err, Query: []byte(query)} 196 } 197 defer tables.Close() 198 for tables.Next() { 199 var table string 200 if err := tables.Scan(&table); err != nil { 201 return err 202 } 203 204 query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table 205 206 if _, err := ch.conn.Exec(query); err != nil { 207 return &database.Error{OrigErr: err, Query: []byte(query)} 208 } 209 } 210 return ch.ensureVersionTable() 211 } 212 213 func (ch *ClickHouse) Lock() error { return nil } 214 func (ch *ClickHouse) Unlock() error { return nil } 215 func (ch *ClickHouse) Close() error { return ch.conn.Close() }