github.com/SigNoz/golang-migrate/v4@v4.0.0-20231005133642-7493dbaf5f5b/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 := fmt.Sprintf( 196 "INSERT INTO %s (version, dirty, sequence) VALUES (%d, %d, %d)", 197 ch.config.MigrationsTable, 198 version, 199 bool(dirty), 200 time.Now().UnixNano(), 201 ) 202 if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil { 203 return &database.Error{OrigErr: err, Query: []byte(query)} 204 } 205 206 return tx.Commit() 207 } 208 209 // ensureVersionTable checks if versions table exists and, if not, creates it. 210 // Note that this function locks the database, which deviates from the usual 211 // convention of "caller locks" in the ClickHouse type. 212 func (ch *ClickHouse) ensureVersionTable() (err error) { 213 if err = ch.Lock(); err != nil { 214 return err 215 } 216 217 defer func() { 218 if e := ch.Unlock(); e != nil { 219 if err == nil { 220 err = e 221 } else { 222 err = multierror.Append(err, e) 223 } 224 } 225 }() 226 227 var ( 228 table string 229 query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'" 230 ) 231 // check if migration table exists 232 if err := ch.conn.QueryRow(query).Scan(&table); err != nil { 233 if err != sql.ErrNoRows { 234 return &database.Error{OrigErr: err, Query: []byte(query)} 235 } 236 } else { 237 return nil 238 } 239 240 // if not, create the empty migration table 241 if len(ch.config.ClusterName) > 0 { 242 query = fmt.Sprintf(` 243 CREATE TABLE %s ON CLUSTER %s ( 244 version Int64, 245 dirty UInt8, 246 sequence UInt64 247 ) Engine=%s`, ch.config.MigrationsTable, ch.config.ClusterName, ch.config.MigrationsTableEngine) 248 } else { 249 query = fmt.Sprintf(` 250 CREATE TABLE %s ( 251 version Int64, 252 dirty UInt8, 253 sequence UInt64 254 ) Engine=%s`, ch.config.MigrationsTable, ch.config.MigrationsTableEngine) 255 } 256 257 if strings.HasSuffix(ch.config.MigrationsTableEngine, "Tree") { 258 query = fmt.Sprintf(`%s ORDER BY sequence`, query) 259 } 260 261 if _, err := ch.conn.Exec(query); err != nil { 262 return &database.Error{OrigErr: err, Query: []byte(query)} 263 } 264 return nil 265 } 266 267 func (ch *ClickHouse) Drop() (err error) { 268 query := "SHOW TABLES FROM " + ch.config.DatabaseName 269 tables, err := ch.conn.Query(query) 270 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 func (ch *ClickHouse) Unlock() error { 307 if !ch.isLocked.CAS(true, false) { 308 return database.ErrNotLocked 309 } 310 311 return nil 312 } 313 func (ch *ClickHouse) Close() error { return ch.conn.Close() }