• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

golang-migrate / migrate / 16295271531

15 Jul 2025 01:55PM UTC coverage: 56.553% (+0.2%) from 56.314%
16295271531

Pull #1294

github

dsyers
lint
Pull Request #1294: Triggers

790 of 1325 new or added lines in 24 files covered. (59.62%)

4 existing lines in 4 files now uncovered.

5277 of 9331 relevant lines covered (56.55%)

55.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.22
/database/sqlcipher/sqlcipher.go
1
package sqlcipher
2

3
import (
4
        "database/sql"
5
        "fmt"
6
        "io"
7
        nurl "net/url"
8
        "strconv"
9
        "strings"
10

11
        "go.uber.org/atomic"
12

13
        "github.com/golang-migrate/migrate/v4"
14
        "github.com/golang-migrate/migrate/v4/database"
15
        "github.com/hashicorp/go-multierror"
16
        _ "github.com/mutecomm/go-sqlcipher/v4"
17
)
18

19
func init() {
2✔
20
        database.Register("sqlcipher", &Sqlite{})
2✔
21
}
2✔
22

23
var DefaultMigrationsTable = "schema_migrations"
24
var (
25
        ErrDatabaseDirty  = fmt.Errorf("database is dirty")
26
        ErrNilConfig      = fmt.Errorf("no config")
27
        ErrNoDatabaseName = fmt.Errorf("no database name")
28
)
29

30
type Config struct {
31
        MigrationsTable string
32
        DatabaseName    string
33
        NoTxWrap        bool
34

35
        Triggers map[string]func(response interface{}) error
36
}
37

38
type Sqlite struct {
39
        db       *sql.DB
40
        isLocked atomic.Bool
41

42
        config *Config
43
}
44

45
type TriggerResponse struct {
46
        Driver  *Sqlite
47
        Config  *Config
48
        Trigger string
49
        Detail  interface{}
50
}
51

52
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
8✔
53
        if config == nil {
8✔
54
                return nil, ErrNilConfig
×
55
        }
×
56

57
        if err := instance.Ping(); err != nil {
8✔
58
                return nil, err
×
59
        }
×
60

61
        if len(config.MigrationsTable) == 0 {
10✔
62
                config.MigrationsTable = DefaultMigrationsTable
2✔
63
        }
2✔
64

65
        mx := &Sqlite{
8✔
66
                db:     instance,
8✔
67
                config: config,
8✔
68
        }
8✔
69
        if err := mx.ensureVersionTable(); err != nil {
8✔
70
                return nil, err
×
71
        }
×
72
        return mx, nil
8✔
73
}
74

75
// ensureVersionTable checks if versions table exists and, if not, creates it.
76
// Note that this function locks the database, which deviates from the usual
77
// convention of "caller locks" in the Sqlite type.
78
func (m *Sqlite) ensureVersionTable() (err error) {
8✔
79
        if err = m.Lock(); err != nil {
8✔
80
                return err
×
81
        }
×
82

83
        defer func() {
16✔
84
                if e := m.Unlock(); e != nil {
8✔
85
                        if err == nil {
×
86
                                err = e
×
87
                        } else {
×
88
                                err = multierror.Append(err, e)
×
89
                        }
×
90
                }
91
        }()
92

93
        if err := m.Trigger(database.TrigVersionTablePre, nil); err != nil {
8✔
NEW
94
                return &database.Error{OrigErr: err, Err: "failed to trigger VersionTablePre"}
×
NEW
95
        }
×
96

97
        query := fmt.Sprintf(`
8✔
98
        CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
8✔
99
  CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
8✔
100
  `, m.config.MigrationsTable, m.config.MigrationsTable)
8✔
101

8✔
102
        if _, err := m.db.Exec(query); err != nil {
8✔
103
                return err
×
104
        }
×
105

106
        if err := m.Trigger(database.TrigVersionTablePost, nil); err != nil {
8✔
NEW
107
                return &database.Error{OrigErr: err, Err: "failed to trigger VersionTablePost"}
×
NEW
108
        }
×
109

110
        return nil
8✔
111
}
112

113
func (m *Sqlite) Open(url string) (database.Driver, error) {
6✔
114
        purl, err := nurl.Parse(url)
6✔
115
        if err != nil {
6✔
116
                return nil, err
×
117
        }
×
118
        dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "sqlite3://", "", 1)
6✔
119
        db, err := sql.Open("sqlite3", dbfile)
6✔
120
        if err != nil {
6✔
121
                return nil, err
×
122
        }
×
123

124
        qv := purl.Query()
6✔
125

6✔
126
        migrationsTable := qv.Get("x-migrations-table")
6✔
127
        if len(migrationsTable) == 0 {
12✔
128
                migrationsTable = DefaultMigrationsTable
6✔
129
        }
6✔
130

131
        noTxWrap := false
6✔
132
        if v := qv.Get("x-no-tx-wrap"); v != "" {
10✔
133
                noTxWrap, err = strconv.ParseBool(v)
4✔
134
                if err != nil {
6✔
135
                        return nil, fmt.Errorf("x-no-tx-wrap: %s", err)
2✔
136
                }
2✔
137
        }
138

139
        mx, err := WithInstance(db, &Config{
4✔
140
                DatabaseName:    purl.Path,
4✔
141
                MigrationsTable: migrationsTable,
4✔
142
                NoTxWrap:        noTxWrap,
4✔
143
        })
4✔
144
        if err != nil {
4✔
145
                return nil, err
×
146
        }
×
147
        return mx, nil
4✔
148
}
149

150
func (m *Sqlite) Close() error {
×
151
        return m.db.Close()
×
152
}
×
153

154
func (m *Sqlite) AddTriggers(t map[string]func(response interface{}) error) {
2✔
155
        m.config.Triggers = t
2✔
156
}
2✔
157

158
func (m *Sqlite) Trigger(name string, detail interface{}) error {
120✔
159
        if m.config.Triggers == nil {
216✔
160
                return nil
96✔
161
        }
96✔
162

163
        if trigger, ok := m.config.Triggers[name]; ok {
32✔
164
                return trigger(TriggerResponse{
8✔
165
                        Driver:  m,
8✔
166
                        Config:  m.config,
8✔
167
                        Trigger: name,
8✔
168
                        Detail:  detail,
8✔
169
                })
8✔
170
        }
8✔
171

172
        return nil
16✔
173
}
174

175
func (m *Sqlite) Drop() (err error) {
6✔
176
        query := `SELECT name FROM sqlite_master WHERE type = 'table';`
6✔
177
        tables, err := m.db.Query(query)
6✔
178
        if err != nil {
6✔
179
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
180
        }
×
181
        defer func() {
12✔
182
                if errClose := tables.Close(); errClose != nil {
6✔
183
                        err = multierror.Append(err, errClose)
×
184
                }
×
185
        }()
186

187
        tableNames := make([]string, 0)
6✔
188
        for tables.Next() {
18✔
189
                var tableName string
12✔
190
                if err := tables.Scan(&tableName); err != nil {
12✔
191
                        return err
×
192
                }
×
193
                if len(tableName) > 0 {
24✔
194
                        tableNames = append(tableNames, tableName)
12✔
195
                }
12✔
196
        }
197
        if err := tables.Err(); err != nil {
6✔
198
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
199
        }
×
200

201
        if len(tableNames) > 0 {
12✔
202
                for _, t := range tableNames {
18✔
203
                        query := "DROP TABLE " + t
12✔
204
                        err = m.executeQuery(query)
12✔
205
                        if err != nil {
12✔
206
                                return &database.Error{OrigErr: err, Query: []byte(query)}
×
207
                        }
×
208
                }
209
                query := "VACUUM"
6✔
210
                _, err = m.db.Query(query)
6✔
211
                if err != nil {
6✔
212
                        return &database.Error{OrigErr: err, Query: []byte(query)}
×
213
                }
×
214
        }
215

216
        return nil
6✔
217
}
218

219
func (m *Sqlite) Lock() error {
26✔
220
        if !m.isLocked.CAS(false, true) {
30✔
221
                return database.ErrLocked
4✔
222
        }
4✔
223
        return nil
22✔
224
}
225

226
func (m *Sqlite) Unlock() error {
22✔
227
        if !m.isLocked.CAS(true, false) {
22✔
228
                return database.ErrNotLocked
×
229
        }
×
230
        return nil
22✔
231
}
232

233
func (m *Sqlite) Run(migration io.Reader) error {
12✔
234
        migr, err := io.ReadAll(migration)
12✔
235
        if err != nil {
12✔
236
                return err
×
237
        }
×
238
        query := string(migr[:])
12✔
239

12✔
240
        if err := m.Trigger(database.TrigRunPre, struct {
12✔
241
                Query string
12✔
242
        }{Query: query}); err != nil {
12✔
NEW
243
                return &database.Error{OrigErr: err, Err: "failed to trigger RunPre"}
×
NEW
244
        }
×
245

246
        if m.config.NoTxWrap {
14✔
247
                if err = m.executeQueryNoTx(query); err != nil {
2✔
NEW
248
                        return err
×
NEW
249
                }
×
250
        } else {
10✔
251
                if err = m.executeQuery(query); err != nil {
10✔
NEW
252
                        return err
×
NEW
253
                }
×
254
        }
255

256
        if err := m.Trigger(database.TrigRunPost, struct {
12✔
257
                Query string
12✔
258
        }{Query: query}); err != nil {
12✔
NEW
259
                return &database.Error{OrigErr: err, Err: "failed to trigger RunPost"}
×
NEW
260
        }
×
261

262
        return nil
12✔
263
}
264

265
func (m *Sqlite) executeQuery(query string) error {
22✔
266
        tx, err := m.db.Begin()
22✔
267
        if err != nil {
22✔
268
                return &database.Error{OrigErr: err, Err: "transaction start failed"}
×
269
        }
×
270
        if _, err := tx.Exec(query); err != nil {
22✔
271
                if errRollback := tx.Rollback(); errRollback != nil {
×
272
                        err = multierror.Append(err, errRollback)
×
273
                }
×
274
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
275
        }
276
        if err := tx.Commit(); err != nil {
22✔
277
                return &database.Error{OrigErr: err, Err: "transaction commit failed"}
×
278
        }
×
279
        return nil
22✔
280
}
281

282
func (m *Sqlite) executeQueryNoTx(query string) error {
2✔
283
        if _, err := m.db.Exec(query); err != nil {
2✔
284
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
285
        }
×
286
        return nil
2✔
287
}
288

289
func (m *Sqlite) SetVersion(version int, dirty bool) error {
40✔
290
        tx, err := m.db.Begin()
40✔
291
        if err != nil {
40✔
292
                return &database.Error{OrigErr: err, Err: "transaction start failed"}
×
293
        }
×
294

295
        if err := m.Trigger(database.TrigSetVersionPre, struct {
40✔
296
                Version int
40✔
297
                Dirty   bool
40✔
298
        }{Version: version, Dirty: dirty}); err != nil {
40✔
NEW
299
                if errRollback := tx.Rollback(); errRollback != nil {
×
NEW
300
                        err = multierror.Append(err, errRollback)
×
NEW
301
                }
×
NEW
302
                return &database.Error{OrigErr: err, Err: "failed to trigger SetVersionPre"}
×
303
        }
304

305
        query := "DELETE FROM " + m.config.MigrationsTable
40✔
306
        if _, err := tx.Exec(query); err != nil {
40✔
307
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
308
        }
×
309

310
        // Also re-write the schema version for nil dirty versions to prevent
311
        // empty schema version for failed down migration on the first migration
312
        // See: https://github.com/golang-migrate/migrate/issues/330
313
        if version >= 0 || (version == database.NilVersion && dirty) {
76✔
314
                query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
36✔
315
                if _, err := tx.Exec(query, version, dirty); err != nil {
36✔
316
                        if errRollback := tx.Rollback(); errRollback != nil {
×
317
                                err = multierror.Append(err, errRollback)
×
318
                        }
×
319
                        return &database.Error{OrigErr: err, Query: []byte(query)}
×
320
                }
321
        }
322

323
        if err := m.Trigger(database.TrigSetVersionPost, struct {
40✔
324
                Version int
40✔
325
                Dirty   bool
40✔
326
        }{Version: version, Dirty: dirty}); err != nil {
40✔
NEW
327
                if errRollback := tx.Rollback(); errRollback != nil {
×
NEW
328
                        err = multierror.Append(err, errRollback)
×
NEW
329
                }
×
NEW
330
                return &database.Error{OrigErr: err, Err: "failed to trigger SetVersionPost"}
×
331
        }
332

333
        if err := tx.Commit(); err != nil {
40✔
334
                return &database.Error{OrigErr: err, Err: "transaction commit failed"}
×
335
        }
×
336

337
        return nil
40✔
338
}
339

340
func (m *Sqlite) Version() (version int, dirty bool, err error) {
32✔
341
        query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
32✔
342
        err = m.db.QueryRow(query).Scan(&version, &dirty)
32✔
343
        if err != nil {
44✔
344
                return database.NilVersion, false, nil
12✔
345
        }
12✔
346
        return version, dirty, nil
20✔
347
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc