github.com/Systemnick/migrate@v3.5.4+incompatible/database/cockroachdb/cockroachdb.go (about) 1 package cockroachdb 2 3 import ( 4 "context" 5 "database/sql" 6 "fmt" 7 "io" 8 "io/ioutil" 9 nurl "net/url" 10 "regexp" 11 "strconv" 12 ) 13 14 import ( 15 "github.com/cockroachdb/cockroach-go/crdb" 16 "github.com/lib/pq" 17 ) 18 19 import ( 20 "github.com/golang-migrate/migrate" 21 "github.com/golang-migrate/migrate/database" 22 ) 23 24 func init() { 25 db := CockroachDb{} 26 database.Register("cockroach", &db) 27 database.Register("cockroachdb", &db) 28 database.Register("crdb-postgres", &db) 29 } 30 31 var DefaultMigrationsTable = "schema_migrations" 32 var DefaultLockTable = "schema_lock" 33 34 var ( 35 ErrNilConfig = fmt.Errorf("no config") 36 ErrNoDatabaseName = fmt.Errorf("no database name") 37 ) 38 39 type Config struct { 40 MigrationsTable string 41 LockTable string 42 ForceLock bool 43 DatabaseName string 44 } 45 46 type CockroachDb struct { 47 db *sql.DB 48 isLocked bool 49 50 // Open and WithInstance need to guarantee that config is never nil 51 config *Config 52 } 53 54 func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) { 55 if config == nil { 56 return nil, ErrNilConfig 57 } 58 59 if err := instance.Ping(); err != nil { 60 return nil, err 61 } 62 63 query := `SELECT current_database()` 64 var databaseName string 65 if err := instance.QueryRow(query).Scan(&databaseName); err != nil { 66 return nil, &database.Error{OrigErr: err, Query: []byte(query)} 67 } 68 69 if len(databaseName) == 0 { 70 return nil, ErrNoDatabaseName 71 } 72 73 config.DatabaseName = databaseName 74 75 if len(config.MigrationsTable) == 0 { 76 config.MigrationsTable = DefaultMigrationsTable 77 } 78 79 if len(config.LockTable) == 0 { 80 config.LockTable = DefaultLockTable 81 } 82 83 px := &CockroachDb{ 84 db: instance, 85 config: config, 86 } 87 88 if err := px.ensureVersionTable(); err != nil { 89 return nil, err 90 } 91 92 if err := px.ensureLockTable(); err != nil { 93 return nil, err 94 } 95 96 return px, nil 97 } 98 99 func (c *CockroachDb) Open(url string) (database.Driver, error) { 100 purl, err := nurl.Parse(url) 101 if err != nil { 102 return nil, err 103 } 104 105 // As Cockroach uses the postgres protocol, and 'postgres' is already a registered database, we need to replace the 106 // connect prefix, with the actual protocol, so that the library can differentiate between the implementations 107 re := regexp.MustCompile("^(cockroach(db)?|crdb-postgres)") 108 connectString := re.ReplaceAllString(migrate.FilterCustomQuery(purl).String(), "postgres") 109 110 db, err := sql.Open("postgres", connectString) 111 if err != nil { 112 return nil, err 113 } 114 115 migrationsTable := purl.Query().Get("x-migrations-table") 116 if len(migrationsTable) == 0 { 117 migrationsTable = DefaultMigrationsTable 118 } 119 120 lockTable := purl.Query().Get("x-lock-table") 121 if len(lockTable) == 0 { 122 lockTable = DefaultLockTable 123 } 124 125 forceLockQuery := purl.Query().Get("x-force-lock") 126 forceLock, err := strconv.ParseBool(forceLockQuery) 127 if err != nil { 128 forceLock = false 129 } 130 131 px, err := WithInstance(db, &Config{ 132 DatabaseName: purl.Path, 133 MigrationsTable: migrationsTable, 134 LockTable: lockTable, 135 ForceLock: forceLock, 136 }) 137 if err != nil { 138 return nil, err 139 } 140 141 return px, nil 142 } 143 144 func (c *CockroachDb) Close() error { 145 return c.db.Close() 146 } 147 148 // Locking is done manually with a separate lock table. Implementing advisory locks in CRDB is being discussed 149 // See: https://github.com/cockroachdb/cockroach/issues/13546 150 func (c *CockroachDb) Lock() error { 151 err := crdb.ExecuteTx(context.Background(), c.db, nil, func(tx *sql.Tx) error { 152 aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) 153 if err != nil { 154 return err 155 } 156 157 query := "SELECT * FROM " + c.config.LockTable + " WHERE lock_id = $1" 158 rows, err := tx.Query(query, aid) 159 if err != nil { 160 return database.Error{OrigErr: err, Err: "failed to fetch migration lock", Query: []byte(query)} 161 } 162 defer rows.Close() 163 164 // If row exists at all, lock is present 165 locked := rows.Next() 166 if locked && !c.config.ForceLock { 167 return database.ErrLocked 168 } 169 170 query = "INSERT INTO " + c.config.LockTable + " (lock_id) VALUES ($1)" 171 if _, err := tx.Exec(query, aid); err != nil { 172 return database.Error{OrigErr: err, Err: "failed to set migration lock", Query: []byte(query)} 173 } 174 175 return nil 176 }) 177 178 if err != nil { 179 return err 180 } else { 181 c.isLocked = true 182 return nil 183 } 184 } 185 186 // Locking is done manually with a separate lock table. Implementing advisory locks in CRDB is being discussed 187 // See: https://github.com/cockroachdb/cockroach/issues/13546 188 func (c *CockroachDb) Unlock() error { 189 aid, err := database.GenerateAdvisoryLockId(c.config.DatabaseName) 190 if err != nil { 191 return err 192 } 193 194 // In the event of an implementation (non-migration) error, it is possible for the lock to not be released. Until 195 // a better locking mechanism is added, a manual purging of the lock table may be required in such circumstances 196 query := "DELETE FROM " + c.config.LockTable + " WHERE lock_id = $1" 197 if _, err := c.db.Exec(query, aid); err != nil { 198 if e, ok := err.(*pq.Error); ok { 199 // 42P01 is "UndefinedTableError" in CockroachDB 200 // https://github.com/cockroachdb/cockroach/blob/master/pkg/sql/pgwire/pgerror/codes.go 201 if e.Code == "42P01" { 202 // On drops, the lock table is fully removed; This is fine, and is a valid "unlocked" state for the schema 203 c.isLocked = false 204 return nil 205 } 206 } 207 return database.Error{OrigErr: err, Err: "failed to release migration lock", Query: []byte(query)} 208 } 209 210 c.isLocked = false 211 return nil 212 } 213 214 func (c *CockroachDb) Run(migration io.Reader) error { 215 migr, err := ioutil.ReadAll(migration) 216 if err != nil { 217 return err 218 } 219 220 // run migration 221 query := string(migr[:]) 222 if _, err := c.db.Exec(query); err != nil { 223 return database.Error{OrigErr: err, Err: "migration failed", Query: migr} 224 } 225 226 return nil 227 } 228 229 func (c *CockroachDb) SetVersion(version int, dirty bool) error { 230 return crdb.ExecuteTx(context.Background(), c.db, nil, func(tx *sql.Tx) error { 231 if _, err := tx.Exec(`DELETE FROM "` + c.config.MigrationsTable + `"`); err != nil { 232 return err 233 } 234 235 if version >= 0 { 236 if _, err := tx.Exec(`INSERT INTO "`+c.config.MigrationsTable+`" (version, dirty) VALUES ($1, $2)`, version, dirty); err != nil { 237 return err 238 } 239 } 240 241 return nil 242 }) 243 } 244 245 func (c *CockroachDb) Version() (version int, dirty bool, err error) { 246 query := `SELECT version, dirty FROM "` + c.config.MigrationsTable + `" LIMIT 1` 247 err = c.db.QueryRow(query).Scan(&version, &dirty) 248 249 switch { 250 case err == sql.ErrNoRows: 251 return database.NilVersion, false, nil 252 253 case err != nil: 254 if e, ok := err.(*pq.Error); ok { 255 // 42P01 is "UndefinedTableError" in CockroachDB 256 // https://github.com/cockroachdb/cockroach/blob/master/pkg/sql/pgwire/pgerror/codes.go 257 if e.Code == "42P01" { 258 return database.NilVersion, false, nil 259 } 260 } 261 return 0, false, &database.Error{OrigErr: err, Query: []byte(query)} 262 263 default: 264 return version, dirty, nil 265 } 266 } 267 268 func (c *CockroachDb) Drop() error { 269 // select all tables in current schema 270 query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema())` 271 tables, err := c.db.Query(query) 272 if err != nil { 273 return &database.Error{OrigErr: err, Query: []byte(query)} 274 } 275 defer tables.Close() 276 277 // delete one table after another 278 tableNames := make([]string, 0) 279 for tables.Next() { 280 var tableName string 281 if err := tables.Scan(&tableName); err != nil { 282 return err 283 } 284 if len(tableName) > 0 { 285 tableNames = append(tableNames, tableName) 286 } 287 } 288 289 if len(tableNames) > 0 { 290 // delete one by one ... 291 for _, t := range tableNames { 292 query = `DROP TABLE IF EXISTS ` + t + ` CASCADE` 293 if _, err := c.db.Exec(query); err != nil { 294 return &database.Error{OrigErr: err, Query: []byte(query)} 295 } 296 } 297 if err := c.ensureVersionTable(); err != nil { 298 return err 299 } 300 } 301 302 return nil 303 } 304 305 func (c *CockroachDb) ensureVersionTable() error { 306 // check if migration table exists 307 var count int 308 query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1` 309 if err := c.db.QueryRow(query, c.config.MigrationsTable).Scan(&count); err != nil { 310 return &database.Error{OrigErr: err, Query: []byte(query)} 311 } 312 if count == 1 { 313 return nil 314 } 315 316 // if not, create the empty migration table 317 query = `CREATE TABLE "` + c.config.MigrationsTable + `" (version INT NOT NULL PRIMARY KEY, dirty BOOL NOT NULL)` 318 if _, err := c.db.Exec(query); err != nil { 319 return &database.Error{OrigErr: err, Query: []byte(query)} 320 } 321 return nil 322 } 323 324 func (c *CockroachDb) ensureLockTable() error { 325 // check if lock table exists 326 var count int 327 query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1` 328 if err := c.db.QueryRow(query, c.config.LockTable).Scan(&count); err != nil { 329 return &database.Error{OrigErr: err, Query: []byte(query)} 330 } 331 if count == 1 { 332 return nil 333 } 334 335 // if not, create the empty lock table 336 query = `CREATE TABLE "` + c.config.LockTable + `" (lock_id INT NOT NULL PRIMARY KEY)` 337 if _, err := c.db.Exec(query); err != nil { 338 return &database.Error{OrigErr: err, Query: []byte(query)} 339 } 340 341 return nil 342 }