Skip to content

Commit 85d673f

Browse files
committed
Add database driver for sqlcipher
This is a copy of the existing sqlite3 driver, except that it imports `_ "github.com/mutecomm/go-sqlcipher/v4"` as database driver. This allows to use migrate with encrypted sqlite databases.
1 parent f1cf81e commit 85d673f

File tree

9 files changed

+440
-0
lines changed

9 files changed

+440
-0
lines changed

database/sqlcipher/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# sqlcipher
2+
3+
This is just a copy of the sqlite3 driver except that it imports `github.com/mutecomm/go-sqlcipher`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE IF EXISTS pets;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE pets (
2+
name string
3+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE IF EXISTS pets;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE pets ADD predator bool;

database/sqlcipher/sqlcipher.go

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

0 commit comments

Comments
 (0)