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  }