github.com/nokia/migrate/v4@v4.16.0/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 "strconv" 10 "strings" 11 "time" 12 13 "go.uber.org/atomic" 14 15 "github.com/hashicorp/go-multierror" 16 "github.com/nokia/migrate/v4" 17 "github.com/nokia/migrate/v4/database" 18 "github.com/nokia/migrate/v4/database/multistmt" 19 "github.com/nokia/migrate/v4/source" 20 ) 21 22 var ( 23 multiStmtDelimiter = []byte(";") 24 25 DefaultMigrationsTable = "schema_migrations" 26 DefaultMigrationsTableEngine = "TinyLog" 27 DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB 28 29 ErrNilConfig = fmt.Errorf("no config") 30 ) 31 32 type Config struct { 33 DatabaseName string 34 ClusterName string 35 MigrationsTable string 36 MigrationsTableEngine string 37 MultiStatementEnabled bool 38 MultiStatementMaxSize int 39 } 40 41 func init() { 42 database.Register("clickhouse", &ClickHouse{}) 43 } 44 45 func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) { 46 if config == nil { 47 return nil, ErrNilConfig 48 } 49 50 if err := conn.Ping(); err != nil { 51 return nil, err 52 } 53 54 ch := &ClickHouse{ 55 conn: conn, 56 config: config, 57 } 58 59 if err := ch.init(); err != nil { 60 return nil, err 61 } 62 63 return ch, nil 64 } 65 66 type ClickHouse struct { 67 conn *sql.DB 68 config *Config 69 isLocked atomic.Bool 70 } 71 72 func (ch *ClickHouse) Open(dsn string) (database.Driver, error) { 73 purl, err := url.Parse(dsn) 74 if err != nil { 75 return nil, err 76 } 77 q := migrate.FilterCustomQuery(purl) 78 q.Scheme = "tcp" 79 conn, err := sql.Open("clickhouse", q.String()) 80 if err != nil { 81 return nil, err 82 } 83 84 multiStatementMaxSize := DefaultMultiStatementMaxSize 85 if s := purl.Query().Get("x-multi-statement-max-size"); len(s) > 0 { 86 multiStatementMaxSize, err = strconv.Atoi(s) 87 if err != nil { 88 return nil, err 89 } 90 } 91 92 migrationsTableEngine := DefaultMigrationsTableEngine 93 if s := purl.Query().Get("x-migrations-table-engine"); len(s) > 0 { 94 migrationsTableEngine = s 95 } 96 97 ch = &ClickHouse{ 98 conn: conn, 99 config: &Config{ 100 MigrationsTable: purl.Query().Get("x-migrations-table"), 101 MigrationsTableEngine: migrationsTableEngine, 102 DatabaseName: purl.Query().Get("database"), 103 ClusterName: purl.Query().Get("x-cluster-name"), 104 MultiStatementEnabled: purl.Query().Get("x-multi-statement") == "true", 105 MultiStatementMaxSize: multiStatementMaxSize, 106 }, 107 } 108 109 if err := ch.init(); err != nil { 110 return nil, err 111 } 112 113 return ch, nil 114 } 115 116 func (ch *ClickHouse) init() error { 117 if len(ch.config.DatabaseName) == 0 { 118 if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil { 119 return err 120 } 121 } 122 123 if len(ch.config.MigrationsTable) == 0 { 124 ch.config.MigrationsTable = DefaultMigrationsTable 125 } 126 127 if ch.config.MultiStatementMaxSize <= 0 { 128 ch.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize 129 } 130 131 if len(ch.config.MigrationsTableEngine) == 0 { 132 ch.config.MigrationsTableEngine = DefaultMigrationsTableEngine 133 } 134 135 return ch.ensureVersionTable() 136 } 137 138 func (ch *ClickHouse) Run(r io.Reader) error { 139 if ch.config.MultiStatementEnabled { 140 var err error 141 if e := multistmt.Parse(r, multiStmtDelimiter, ch.config.MultiStatementMaxSize, func(m []byte) bool { 142 tq := strings.TrimSpace(string(m)) 143 if tq == "" { 144 return true 145 } 146 if _, e := ch.conn.Exec(string(m)); e != nil { 147 err = database.Error{OrigErr: e, Err: "migration failed", Query: m} 148 return false 149 } 150 return true 151 }); e != nil { 152 return e 153 } 154 return err 155 } 156 157 migration, err := ioutil.ReadAll(r) 158 if err != nil { 159 return err 160 } 161 162 if _, err := ch.conn.Exec(string(migration)); err != nil { 163 return database.Error{OrigErr: err, Err: "migration failed", Query: migration} 164 } 165 166 return nil 167 } 168 169 func (ch *ClickHouse) RunFunctionMigration(fn source.MigrationFunc) error { 170 return database.ErrNotImpl 171 } 172 173 func (ch *ClickHouse) Version() (int, bool, error) { 174 var ( 175 version int 176 dirty uint8 177 query = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1" 178 ) 179 if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil { 180 if err == sql.ErrNoRows { 181 return database.NilVersion, false, nil 182 } 183 return 0, false, &database.Error{OrigErr: err, Query: []byte(query)} 184 } 185 return version, dirty == 1, nil 186 } 187 188 func (ch *ClickHouse) SetVersion(version int, dirty bool) error { 189 var ( 190 bool = func(v bool) uint8 { 191 if v { 192 return 1 193 } 194 return 0 195 } 196 tx, err = ch.conn.Begin() 197 ) 198 if err != nil { 199 return err 200 } 201 202 query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)" 203 if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil { 204 return &database.Error{OrigErr: err, Query: []byte(query)} 205 } 206 207 return tx.Commit() 208 } 209 210 // ensureVersionTable checks if versions table exists and, if not, creates it. 211 // Note that this function locks the database, which deviates from the usual 212 // convention of "caller locks" in the ClickHouse type. 213 func (ch *ClickHouse) ensureVersionTable() (err error) { 214 if err = ch.Lock(); err != nil { 215 return err 216 } 217 218 defer func() { 219 if e := ch.Unlock(); e != nil { 220 if err == nil { 221 err = e 222 } else { 223 err = multierror.Append(err, e) 224 } 225 } 226 }() 227 228 var ( 229 table string 230 query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'" 231 ) 232 // check if migration table exists 233 if err := ch.conn.QueryRow(query).Scan(&table); err != nil { 234 if err != sql.ErrNoRows { 235 return &database.Error{OrigErr: err, Query: []byte(query)} 236 } 237 } else { 238 return nil 239 } 240 241 // if not, create the empty migration table 242 if len(ch.config.ClusterName) > 0 { 243 query = fmt.Sprintf(` 244 CREATE TABLE %s ON CLUSTER %s ( 245 version Int64, 246 dirty UInt8, 247 sequence UInt64 248 ) Engine=%s`, ch.config.MigrationsTable, ch.config.ClusterName, ch.config.MigrationsTableEngine) 249 } else { 250 query = fmt.Sprintf(` 251 CREATE TABLE %s ( 252 version Int64, 253 dirty UInt8, 254 sequence UInt64 255 ) Engine=%s`, ch.config.MigrationsTable, ch.config.MigrationsTableEngine) 256 } 257 258 if strings.HasSuffix(ch.config.MigrationsTableEngine, "Tree") { 259 query = fmt.Sprintf(`%s ORDER BY sequence`, query) 260 } 261 262 if _, err := ch.conn.Exec(query); err != nil { 263 return &database.Error{OrigErr: err, Query: []byte(query)} 264 } 265 return nil 266 } 267 268 func (ch *ClickHouse) Drop() (err error) { 269 query := "SHOW TABLES FROM " + ch.config.DatabaseName 270 tables, err := ch.conn.Query(query) 271 if err != nil { 272 return &database.Error{OrigErr: err, Query: []byte(query)} 273 } 274 defer func() { 275 if errClose := tables.Close(); errClose != nil { 276 err = multierror.Append(err, errClose) 277 } 278 }() 279 280 for tables.Next() { 281 var table string 282 if err := tables.Scan(&table); err != nil { 283 return err 284 } 285 286 query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table 287 288 if _, err := ch.conn.Exec(query); err != nil { 289 return &database.Error{OrigErr: err, Query: []byte(query)} 290 } 291 } 292 if err := tables.Err(); err != nil { 293 return &database.Error{OrigErr: err, Query: []byte(query)} 294 } 295 296 return nil 297 } 298 299 func (ch *ClickHouse) Lock() error { 300 if !ch.isLocked.CAS(false, true) { 301 return database.ErrLocked 302 } 303 304 return nil 305 } 306 307 func (ch *ClickHouse) Unlock() error { 308 if !ch.isLocked.CAS(true, false) { 309 return database.ErrNotLocked 310 } 311 312 return nil 313 } 314 func (ch *ClickHouse) Close() error { return ch.conn.Close() }