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