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