• 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.07
/database/sqlite/sqlite.go
1
package sqlite
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
        _ "modernc.org/sqlite"
17
)
18

19
func init() {
2✔
20
        database.Register("sqlite", &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) {
10✔
53
        if config == nil {
10✔
54
                return nil, ErrNilConfig
×
55
        }
×
56

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

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

65
        mx := &Sqlite{
10✔
66
                db:     instance,
10✔
67
                config: config,
10✔
68
        }
10✔
69
        if err := mx.ensureVersionTable(); err != nil {
10✔
70
                return nil, err
×
71
        }
×
72
        return mx, nil
10✔
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) {
10✔
79
        if err = m.Lock(); err != nil {
10✔
80
                return err
×
81
        }
×
82

83
        defer func() {
20✔
84
                if e := m.Unlock(); e != nil {
10✔
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 {
10✔
NEW
94
                return &database.Error{OrigErr: err, Err: "failed to trigger VersionTablePre"}
×
NEW
95
        }
×
96

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

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

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

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

123
        qv := purl.Query()
8✔
124

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

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

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

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

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

157
func (m *Sqlite) Trigger(name string, detail interface{}) error {
152✔
158
        if m.config.Triggers == nil {
280✔
159
                return nil
128✔
160
        }
128✔
161

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

171
        return nil
16✔
172
}
173

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

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

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

215
        return nil
8✔
216
}
217

218
func (m *Sqlite) Lock() error {
34✔
219
        if !m.isLocked.CAS(false, true) {
40✔
220
                return database.ErrLocked
6✔
221
        }
6✔
222
        return nil
28✔
223
}
224

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

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

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

245
        if m.config.NoTxWrap {
16✔
246
                if err = m.executeQueryNoTx(query); err != nil {
2✔
NEW
247
                        return err
×
NEW
248
                }
×
249
        } else if err = m.executeQuery(query); err != nil {
12✔
NEW
250
                return err
×
UNCOV
251
        }
×
252

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

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

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

285
func (m *Sqlite) SetVersion(version int, dirty bool) error {
52✔
286
        tx, err := m.db.Begin()
52✔
287
        if err != nil {
52✔
288
                return &database.Error{OrigErr: err, Err: "transaction start failed"}
×
289
        }
×
290

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

301
        query := "DELETE FROM " + m.config.MigrationsTable
52✔
302
        if _, err := tx.Exec(query); err != nil {
52✔
303
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
304
        }
×
305

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

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

329
        if err := tx.Commit(); err != nil {
52✔
330
                return &database.Error{OrigErr: err, Err: "transaction commit failed"}
×
331
        }
×
332

333
        return nil
52✔
334
}
335

336
func (m *Sqlite) Version() (version int, dirty bool, err error) {
46✔
337
        query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
46✔
338
        err = m.db.QueryRow(query).Scan(&version, &dirty)
46✔
339
        if err != nil {
62✔
340
                return database.NilVersion, false, nil
16✔
341
        }
16✔
342
        return version, dirty, nil
30✔
343
}
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