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 }