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  }