github.com/mrqzzz/migrate@v5.1.7+incompatible/database/redshift/redshift.go (about)

     1  // +build go1.9
     2  
     3  package redshift
     4  
     5  import (
     6  	"context"
     7  	"database/sql"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	nurl "net/url"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/golang-migrate/migrate/v4"
    16  	"github.com/golang-migrate/migrate/v4/database"
    17  	"github.com/lib/pq"
    18  )
    19  
    20  func init() {
    21  	db := Redshift{}
    22  	database.Register("redshift", &db)
    23  }
    24  
    25  var DefaultMigrationsTable = "schema_migrations"
    26  
    27  var (
    28  	ErrNilConfig      = fmt.Errorf("no config")
    29  	ErrNoDatabaseName = fmt.Errorf("no database name")
    30  )
    31  
    32  type Config struct {
    33  	MigrationsTable string
    34  	DatabaseName    string
    35  }
    36  
    37  type Redshift struct {
    38  	isLocked bool
    39  	conn     *sql.Conn
    40  	db       *sql.DB
    41  
    42  	// Open and WithInstance need to garantuee that config is never nil
    43  	config *Config
    44  }
    45  
    46  func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
    47  	if config == nil {
    48  		return nil, ErrNilConfig
    49  	}
    50  
    51  	if err := instance.Ping(); err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	query := `SELECT CURRENT_DATABASE()`
    56  	var databaseName string
    57  	if err := instance.QueryRow(query).Scan(&databaseName); err != nil {
    58  		return nil, &database.Error{OrigErr: err, Query: []byte(query)}
    59  	}
    60  
    61  	if len(databaseName) == 0 {
    62  		return nil, ErrNoDatabaseName
    63  	}
    64  
    65  	config.DatabaseName = databaseName
    66  
    67  	if len(config.MigrationsTable) == 0 {
    68  		config.MigrationsTable = DefaultMigrationsTable
    69  	}
    70  
    71  	conn, err := instance.Conn(context.Background())
    72  
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	px := &Redshift{
    78  		conn:   conn,
    79  		db:     instance,
    80  		config: config,
    81  	}
    82  
    83  	if err := px.ensureVersionTable(); err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	return px, nil
    88  }
    89  
    90  func (p *Redshift) Open(url string) (database.Driver, error) {
    91  	purl, err := nurl.Parse(url)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  	purl.Scheme = "postgres"
    96  
    97  	db, err := sql.Open("postgres", migrate.FilterCustomQuery(purl).String())
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	migrationsTable := purl.Query().Get("x-migrations-table")
   103  	if len(migrationsTable) == 0 {
   104  		migrationsTable = DefaultMigrationsTable
   105  	}
   106  
   107  	px, err := WithInstance(db, &Config{
   108  		DatabaseName:    purl.Path,
   109  		MigrationsTable: migrationsTable,
   110  	})
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	return px, nil
   116  }
   117  
   118  func (p *Redshift) Close() error {
   119  	connErr := p.conn.Close()
   120  	dbErr := p.db.Close()
   121  	if connErr != nil || dbErr != nil {
   122  		return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
   123  	}
   124  	return nil
   125  }
   126  
   127  // Redshift does not support advisory lock functions: https://docs.aws.amazon.com/redshift/latest/dg/c_unsupported-postgresql-functions.html
   128  func (p *Redshift) Lock() error {
   129  	if p.isLocked {
   130  		return database.ErrLocked
   131  	}
   132  	p.isLocked = true
   133  	return nil
   134  }
   135  
   136  func (p *Redshift) Unlock() error {
   137  	p.isLocked = false
   138  	return nil
   139  }
   140  
   141  func (p *Redshift) Run(migration io.Reader) error {
   142  	migr, err := ioutil.ReadAll(migration)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	// run migration
   148  	query := string(migr[:])
   149  	if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
   150  		if pgErr, ok := err.(*pq.Error); ok {
   151  			var line uint
   152  			var col uint
   153  			var lineColOK bool
   154  			if pgErr.Position != "" {
   155  				if pos, err := strconv.ParseUint(pgErr.Position, 10, 64); err == nil {
   156  					line, col, lineColOK = computeLineFromPos(query, int(pos))
   157  				}
   158  			}
   159  			message := fmt.Sprintf("migration failed: %s", pgErr.Message)
   160  			if lineColOK {
   161  				message = fmt.Sprintf("%s (column %d)", message, col)
   162  			}
   163  			if pgErr.Detail != "" {
   164  				message = fmt.Sprintf("%s, %s", message, pgErr.Detail)
   165  			}
   166  			return database.Error{OrigErr: err, Err: message, Query: migr, Line: line}
   167  		}
   168  		return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  func computeLineFromPos(s string, pos int) (line uint, col uint, ok bool) {
   175  	// replace crlf with lf
   176  	s = strings.Replace(s, "\r\n", "\n", -1)
   177  	// pg docs: pos uses index 1 for the first character, and positions are measured in characters not bytes
   178  	runes := []rune(s)
   179  	if pos > len(runes) {
   180  		return 0, 0, false
   181  	}
   182  	sel := runes[:pos]
   183  	line = uint(runesCount(sel, newLine) + 1)
   184  	col = uint(pos - 1 - runesLastIndex(sel, newLine))
   185  	return line, col, true
   186  }
   187  
   188  const newLine = '\n'
   189  
   190  func runesCount(input []rune, target rune) int {
   191  	var count int
   192  	for _, r := range input {
   193  		if r == target {
   194  			count++
   195  		}
   196  	}
   197  	return count
   198  }
   199  
   200  func runesLastIndex(input []rune, target rune) int {
   201  	for i := len(input) - 1; i >= 0; i-- {
   202  		if input[i] == target {
   203  			return i
   204  		}
   205  	}
   206  	return -1
   207  }
   208  
   209  func (p *Redshift) SetVersion(version int, dirty bool) error {
   210  	tx, err := p.conn.BeginTx(context.Background(), &sql.TxOptions{})
   211  	if err != nil {
   212  		return &database.Error{OrigErr: err, Err: "transaction start failed"}
   213  	}
   214  
   215  	query := `DELETE FROM "` + p.config.MigrationsTable + `"`
   216  	if _, err := tx.Exec(query); err != nil {
   217  		tx.Rollback()
   218  		return &database.Error{OrigErr: err, Query: []byte(query)}
   219  	}
   220  
   221  	if version >= 0 {
   222  		query = `INSERT INTO "` + p.config.MigrationsTable + `" (version, dirty) VALUES ($1, $2)`
   223  		if _, err := tx.Exec(query, version, dirty); err != nil {
   224  			tx.Rollback()
   225  			return &database.Error{OrigErr: err, Query: []byte(query)}
   226  		}
   227  	}
   228  
   229  	if err := tx.Commit(); err != nil {
   230  		return &database.Error{OrigErr: err, Err: "transaction commit failed"}
   231  	}
   232  
   233  	return nil
   234  }
   235  
   236  func (p *Redshift) Version() (version int, dirty bool, err error) {
   237  	query := `SELECT version, dirty FROM "` + p.config.MigrationsTable + `" LIMIT 1`
   238  	err = p.conn.QueryRowContext(context.Background(), query).Scan(&version, &dirty)
   239  	switch {
   240  	case err == sql.ErrNoRows:
   241  		return database.NilVersion, false, nil
   242  
   243  	case err != nil:
   244  		if e, ok := err.(*pq.Error); ok {
   245  			if e.Code.Name() == "undefined_table" {
   246  				return database.NilVersion, false, nil
   247  			}
   248  		}
   249  		return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
   250  
   251  	default:
   252  		return version, dirty, nil
   253  	}
   254  }
   255  
   256  func (p *Redshift) Drop() error {
   257  	// select all tables in current schema
   258  	query := `SELECT table_name FROM information_schema.tables WHERE table_schema=(SELECT current_schema()) AND table_type='BASE TABLE'`
   259  	tables, err := p.conn.QueryContext(context.Background(), query)
   260  	if err != nil {
   261  		return &database.Error{OrigErr: err, Query: []byte(query)}
   262  	}
   263  	defer tables.Close()
   264  
   265  	// delete one table after another
   266  	tableNames := make([]string, 0)
   267  	for tables.Next() {
   268  		var tableName string
   269  		if err := tables.Scan(&tableName); err != nil {
   270  			return err
   271  		}
   272  		if len(tableName) > 0 {
   273  			tableNames = append(tableNames, tableName)
   274  		}
   275  	}
   276  
   277  	if len(tableNames) > 0 {
   278  		// delete one by one ...
   279  		for _, t := range tableNames {
   280  			query = `DROP TABLE IF EXISTS ` + t + ` CASCADE`
   281  			if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
   282  				return &database.Error{OrigErr: err, Query: []byte(query)}
   283  			}
   284  		}
   285  		if err := p.ensureVersionTable(); err != nil {
   286  			return err
   287  		}
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func (p *Redshift) ensureVersionTable() error {
   294  	// check if migration table exists
   295  	var count int
   296  	query := `SELECT COUNT(1) FROM information_schema.tables WHERE table_name = $1 AND table_schema = (SELECT current_schema()) LIMIT 1`
   297  	if err := p.conn.QueryRowContext(context.Background(), query, p.config.MigrationsTable).Scan(&count); err != nil {
   298  		return &database.Error{OrigErr: err, Query: []byte(query)}
   299  	}
   300  	if count == 1 {
   301  		return nil
   302  	}
   303  
   304  	// if not, create the empty migration table
   305  	query = `CREATE TABLE "` + p.config.MigrationsTable + `" (version bigint not null primary key, dirty boolean not null)`
   306  	if _, err := p.conn.ExecContext(context.Background(), query); err != nil {
   307  		return &database.Error{OrigErr: err, Query: []byte(query)}
   308  	}
   309  	return nil
   310  }