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