github.com/fr-nvriep/migrate/v4@v4.3.2/database/firebird/firebird.go (about)

     1  // +build go1.9
     2  
     3  package firebird
     4  
     5  import (
     6  	"context"
     7  	"database/sql"
     8  	"fmt"
     9  	"github.com/fr-nvriep/migrate/v4"
    10  	"github.com/fr-nvriep/migrate/v4/database"
    11  	"github.com/hashicorp/go-multierror"
    12  	_ "github.com/nakagami/firebirdsql"
    13  	"io"
    14  	"io/ioutil"
    15  	nurl "net/url"
    16  )
    17  
    18  func init() {
    19  	db := Firebird{}
    20  	database.Register("firebird", &db)
    21  	database.Register("firebirdsql", &db)
    22  }
    23  
    24  var DefaultMigrationsTable = "schema_migrations"
    25  
    26  var (
    27  	ErrNilConfig = fmt.Errorf("no config")
    28  )
    29  
    30  type Config struct {
    31  	DatabaseName    string
    32  	MigrationsTable string
    33  }
    34  
    35  type Firebird struct {
    36  	// Locking and unlocking need to use the same connection
    37  	conn     *sql.Conn
    38  	db       *sql.DB
    39  	isLocked bool
    40  
    41  	// Open and WithInstance need to guarantee that config is never nil
    42  	config *Config
    43  }
    44  
    45  func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
    46  	if config == nil {
    47  		return nil, ErrNilConfig
    48  	}
    49  
    50  	if err := instance.Ping(); err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	if len(config.MigrationsTable) == 0 {
    55  		config.MigrationsTable = DefaultMigrationsTable
    56  	}
    57  
    58  	conn, err := instance.Conn(context.Background())
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	fb := &Firebird{
    64  		conn:   conn,
    65  		db:     instance,
    66  		config: config,
    67  	}
    68  
    69  	if err := fb.ensureVersionTable(); err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	return fb, nil
    74  }
    75  
    76  func (f *Firebird) Open(dsn string) (database.Driver, error) {
    77  	purl, err := nurl.Parse(dsn)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	db, err := sql.Open("firebirdsql", migrate.FilterCustomQuery(purl).String())
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	px, err := WithInstance(db, &Config{
    88  		MigrationsTable: purl.Query().Get("x-migrations-table"),
    89  		DatabaseName:    purl.Path,
    90  	})
    91  
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	return px, nil
    97  }
    98  
    99  func (f *Firebird) Close() error {
   100  	connErr := f.conn.Close()
   101  	dbErr := f.db.Close()
   102  	if connErr != nil || dbErr != nil {
   103  		return fmt.Errorf("conn: %v, db: %v", connErr, dbErr)
   104  	}
   105  	return nil
   106  }
   107  
   108  func (f *Firebird) Lock() error {
   109  	if f.isLocked {
   110  		return database.ErrLocked
   111  	}
   112  	f.isLocked = true
   113  	return nil
   114  }
   115  
   116  func (f *Firebird) Unlock() error {
   117  	f.isLocked = false
   118  	return nil
   119  }
   120  
   121  func (f *Firebird) Run(migration io.Reader) error {
   122  	migr, err := ioutil.ReadAll(migration)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	// run migration
   128  	query := string(migr[:])
   129  	if _, err := f.conn.ExecContext(context.Background(), query); err != nil {
   130  		return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  func (f *Firebird) SetVersion(version int, dirty bool) error {
   137  	if version < 0 {
   138  		return nil
   139  	}
   140  
   141  	query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
   142  					DELETE FROM "%v";
   143  					INSERT INTO "%v" (version, dirty) VALUES (%v, %v);
   144  				END;`,
   145  		f.config.MigrationsTable, f.config.MigrationsTable, version, btoi(dirty))
   146  
   147  	if _, err := f.conn.ExecContext(context.Background(), query, version, btoi(dirty)); err != nil {
   148  		return &database.Error{OrigErr: err, Query: []byte(query)}
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  func (f *Firebird) Version() (version int, dirty bool, err error) {
   155  	var d int
   156  	query := fmt.Sprintf(`SELECT FIRST 1 version, dirty FROM "%v"`, f.config.MigrationsTable)
   157  	err = f.conn.QueryRowContext(context.Background(), query).Scan(&version, &d)
   158  	switch {
   159  	case err == sql.ErrNoRows:
   160  		return database.NilVersion, false, nil
   161  	case err != nil:
   162  		return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
   163  
   164  	default:
   165  		return version, itob(d), nil
   166  	}
   167  }
   168  
   169  func (f *Firebird) Drop() (err error) {
   170  	// select all tables
   171  	query := `SELECT rdb$relation_name FROM rdb$relations WHERE rdb$view_blr IS NULL AND (rdb$system_flag IS NULL OR rdb$system_flag = 0);`
   172  	tables, err := f.conn.QueryContext(context.Background(), query)
   173  	if err != nil {
   174  		return &database.Error{OrigErr: err, Query: []byte(query)}
   175  	}
   176  	defer func() {
   177  		if errClose := tables.Close(); errClose != nil {
   178  			err = multierror.Append(err, errClose)
   179  		}
   180  	}()
   181  
   182  	// delete one table after another
   183  	tableNames := make([]string, 0)
   184  	for tables.Next() {
   185  		var tableName string
   186  		if err := tables.Scan(&tableName); err != nil {
   187  			return err
   188  		}
   189  		if len(tableName) > 0 {
   190  			tableNames = append(tableNames, tableName)
   191  		}
   192  	}
   193  
   194  	// delete one by one ...
   195  	for _, t := range tableNames {
   196  		query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
   197  						if (not exists(select 1 from rdb$relations where rdb$relation_name = '%v')) then
   198  						execute statement 'drop table "%v"';
   199  					END;`,
   200  			t, t)
   201  
   202  		if _, err := f.conn.ExecContext(context.Background(), query); err != nil {
   203  			return &database.Error{OrigErr: err, Query: []byte(query)}
   204  		}
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  // ensureVersionTable checks if versions table exists and, if not, creates it.
   211  func (f *Firebird) ensureVersionTable() (err error) {
   212  	if err = f.Lock(); err != nil {
   213  		return err
   214  	}
   215  
   216  	defer func() {
   217  		if e := f.Unlock(); e != nil {
   218  			if err == nil {
   219  				err = e
   220  			} else {
   221  				err = multierror.Append(err, e)
   222  			}
   223  		}
   224  	}()
   225  
   226  	query := fmt.Sprintf(`EXECUTE BLOCK AS BEGIN
   227  			if (not exists(select 1 from rdb$relations where rdb$relation_name = '%v')) then
   228  			execute statement 'create table "%v" (version bigint not null primary key, dirty smallint not null)';
   229  		END;`,
   230  		f.config.MigrationsTable, f.config.MigrationsTable)
   231  
   232  	if _, err = f.conn.ExecContext(context.Background(), query); err != nil {
   233  		return &database.Error{OrigErr: err, Query: []byte(query)}
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  // btoi converts bool to int
   240  func btoi(v bool) int {
   241  	if v {
   242  		return 1
   243  	}
   244  	return 0
   245  }
   246  
   247  // itob converts int to bool
   248  func itob(v int) bool {
   249  	return v != 0
   250  }