github.com/nokia/migrate/v4@v4.16.0/database/sqlcipher/sqlcipher.go (about)

     1  package sqlcipher
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	nurl "net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"go.uber.org/atomic"
    13  
    14  	"github.com/hashicorp/go-multierror"
    15  	_ "github.com/mutecomm/go-sqlcipher/v4"
    16  	"github.com/nokia/migrate/v4"
    17  	"github.com/nokia/migrate/v4/database"
    18  	"github.com/nokia/migrate/v4/source"
    19  )
    20  
    21  func init() {
    22  	database.Register("sqlcipher", &Sqlite{})
    23  }
    24  
    25  var DefaultMigrationsTable = "schema_migrations"
    26  var (
    27  	ErrDatabaseDirty  = fmt.Errorf("database is dirty")
    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  	NoTxWrap        bool
    36  }
    37  
    38  type Sqlite struct {
    39  	db       *sql.DB
    40  	isLocked atomic.Bool
    41  
    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  	mx := &Sqlite{
    59  		db:     instance,
    60  		config: config,
    61  	}
    62  	if err := mx.ensureVersionTable(); err != nil {
    63  		return nil, err
    64  	}
    65  	return mx, nil
    66  }
    67  
    68  // ensureVersionTable checks if versions table exists and, if not, creates it.
    69  // Note that this function locks the database, which deviates from the usual
    70  // convention of "caller locks" in the Sqlite type.
    71  func (m *Sqlite) ensureVersionTable() (err error) {
    72  	if err = m.Lock(); err != nil {
    73  		return err
    74  	}
    75  
    76  	defer func() {
    77  		if e := m.Unlock(); e != nil {
    78  			if err == nil {
    79  				err = e
    80  			} else {
    81  				err = multierror.Append(err, e)
    82  			}
    83  		}
    84  	}()
    85  
    86  	query := fmt.Sprintf(`
    87  	CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
    88    CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
    89    `, m.config.MigrationsTable, m.config.MigrationsTable)
    90  
    91  	if _, err := m.db.Exec(query); err != nil {
    92  		return err
    93  	}
    94  	return nil
    95  }
    96  
    97  func (m *Sqlite) Open(url string) (database.Driver, error) {
    98  	purl, err := nurl.Parse(url)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "sqlite3://", "", 1)
   103  	db, err := sql.Open("sqlite3", dbfile)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	qv := purl.Query()
   109  
   110  	migrationsTable := qv.Get("x-migrations-table")
   111  	if len(migrationsTable) == 0 {
   112  		migrationsTable = DefaultMigrationsTable
   113  	}
   114  
   115  	noTxWrap := false
   116  	if v := qv.Get("x-no-tx-wrap"); v != "" {
   117  		noTxWrap, err = strconv.ParseBool(v)
   118  		if err != nil {
   119  			return nil, fmt.Errorf("x-no-tx-wrap: %s", err)
   120  		}
   121  	}
   122  
   123  	mx, err := WithInstance(db, &Config{
   124  		DatabaseName:    purl.Path,
   125  		MigrationsTable: migrationsTable,
   126  		NoTxWrap:        noTxWrap,
   127  	})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return mx, nil
   132  }
   133  
   134  func (m *Sqlite) Close() error {
   135  	return m.db.Close()
   136  }
   137  
   138  func (m *Sqlite) Drop() (err error) {
   139  	query := `SELECT name FROM sqlite_master WHERE type = 'table';`
   140  	tables, err := m.db.Query(query)
   141  	if err != nil {
   142  		return &database.Error{OrigErr: err, Query: []byte(query)}
   143  	}
   144  	defer func() {
   145  		if errClose := tables.Close(); errClose != nil {
   146  			err = multierror.Append(err, errClose)
   147  		}
   148  	}()
   149  
   150  	tableNames := make([]string, 0)
   151  	for tables.Next() {
   152  		var tableName string
   153  		if err := tables.Scan(&tableName); err != nil {
   154  			return err
   155  		}
   156  		if len(tableName) > 0 {
   157  			tableNames = append(tableNames, tableName)
   158  		}
   159  	}
   160  	if err := tables.Err(); err != nil {
   161  		return &database.Error{OrigErr: err, Query: []byte(query)}
   162  	}
   163  
   164  	if len(tableNames) > 0 {
   165  		for _, t := range tableNames {
   166  			query := "DROP TABLE " + t
   167  			err = m.executeQuery(query)
   168  			if err != nil {
   169  				return &database.Error{OrigErr: err, Query: []byte(query)}
   170  			}
   171  		}
   172  		query := "VACUUM"
   173  		_, err = m.db.Query(query)
   174  		if err != nil {
   175  			return &database.Error{OrigErr: err, Query: []byte(query)}
   176  		}
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  func (m *Sqlite) Lock() error {
   183  	if !m.isLocked.CAS(false, true) {
   184  		return database.ErrLocked
   185  	}
   186  	return nil
   187  }
   188  
   189  func (m *Sqlite) Unlock() error {
   190  	if !m.isLocked.CAS(true, false) {
   191  		return database.ErrNotLocked
   192  	}
   193  	return nil
   194  }
   195  
   196  func (m *Sqlite) Run(migration io.Reader) error {
   197  	migr, err := ioutil.ReadAll(migration)
   198  	if err != nil {
   199  		return err
   200  	}
   201  	query := string(migr[:])
   202  
   203  	if m.config.NoTxWrap {
   204  		return m.executeQueryNoTx(query)
   205  	}
   206  	return m.executeQuery(query)
   207  }
   208  
   209  func (m *Sqlite) RunFunctionMigration(fn source.MigrationFunc) error {
   210  	return database.ErrNotImpl
   211  }
   212  
   213  func (m *Sqlite) executeQuery(query string) error {
   214  	tx, err := m.db.Begin()
   215  	if err != nil {
   216  		return &database.Error{OrigErr: err, Err: "transaction start failed"}
   217  	}
   218  	if _, err := tx.Exec(query); err != nil {
   219  		if errRollback := tx.Rollback(); errRollback != nil {
   220  			err = multierror.Append(err, errRollback)
   221  		}
   222  		return &database.Error{OrigErr: err, Query: []byte(query)}
   223  	}
   224  	if err := tx.Commit(); err != nil {
   225  		return &database.Error{OrigErr: err, Err: "transaction commit failed"}
   226  	}
   227  	return nil
   228  }
   229  
   230  func (m *Sqlite) executeQueryNoTx(query string) error {
   231  	if _, err := m.db.Exec(query); err != nil {
   232  		return &database.Error{OrigErr: err, Query: []byte(query)}
   233  	}
   234  	return nil
   235  }
   236  
   237  func (m *Sqlite) SetVersion(version int, dirty bool) error {
   238  	tx, err := m.db.Begin()
   239  	if err != nil {
   240  		return &database.Error{OrigErr: err, Err: "transaction start failed"}
   241  	}
   242  
   243  	query := "DELETE FROM " + m.config.MigrationsTable
   244  	if _, err := tx.Exec(query); err != nil {
   245  		return &database.Error{OrigErr: err, Query: []byte(query)}
   246  	}
   247  
   248  	// Also re-write the schema version for nil dirty versions to prevent
   249  	// empty schema version for failed down migration on the first migration
   250  	// See: https://github.com/nokia/migrate/issues/330
   251  	if version >= 0 || (version == database.NilVersion && dirty) {
   252  		query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
   253  		if _, err := tx.Exec(query, version, dirty); err != nil {
   254  			if errRollback := tx.Rollback(); errRollback != nil {
   255  				err = multierror.Append(err, errRollback)
   256  			}
   257  			return &database.Error{OrigErr: err, Query: []byte(query)}
   258  		}
   259  	}
   260  
   261  	if err := tx.Commit(); err != nil {
   262  		return &database.Error{OrigErr: err, Err: "transaction commit failed"}
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  func (m *Sqlite) Version() (version int, dirty bool, err error) {
   269  	query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
   270  	err = m.db.QueryRow(query).Scan(&version, &dirty)
   271  	if err != nil {
   272  		return database.NilVersion, false, nil
   273  	}
   274  	return version, dirty, nil
   275  }