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