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

golang-migrate / migrate / 24640136570

19 Apr 2026 09:58PM UTC coverage: 56.328% (+1.9%) from 54.432%
24640136570

Pull #1394

github

joschi
fix: address second round of Copilot review comments

- README.md: fix db.system → db.system.name in database span table
- otel.go: call otel.Handle(err) on instrument creation errors instead
  of silently discarding them (doc comment already promised this)
- migrate.go: use attribute.Int64 for uint version fields to avoid
  int overflow on 32-bit platforms
- migrate.go: sanitize database.Error in otelSpanSetError to avoid
  leaking migration SQL into trace span status descriptions
- database/oteldriver.go: same SQL-leak fix in endSpan; add errors import
- source/oteldriver.go: use attribute.Int64 for uint version fields
- source/oteldriver_test.go: assert span.Status().Code != codes.Error
  directly rather than comparing full sdktrace.Status struct
- source/pkger/pkger.go: fix import grouping (stdlib before third-party)
- source/google_cloud_storage/storage.go: merge context into stdlib group

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pull Request #1394: feat: OpenTelemetry instrumentation

1020 of 1236 new or added lines in 47 files covered. (82.52%)

8 existing lines in 7 files now uncovered.

4731 of 8399 relevant lines covered (56.33%)

50.77 hits per line

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

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

3
import (
4
        "context"
5
        "database/sql"
6
        "database/sql/driver"
7
        "errors"
8
        "fmt"
9
        "io"
10
        nurl "net/url"
11
        "strconv"
12
        "strings"
13
        "sync"
14
        "sync/atomic"
15

16
        "github.com/XSAM/otelsql"
17
        "github.com/golang-migrate/migrate/v4"
18
        "github.com/golang-migrate/migrate/v4/database"
19
        _ "github.com/mutecomm/go-sqlcipher/v4"
20
        semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
21
)
22

23
// go-sqlcipher/v4 implements the deprecated driver.Execer interface but not
24
// driver.ExecerContext. Without ExecerContext, database/sql falls back to
25
// Prepare+Exec which only prepares the first statement in a multi-statement
26
// query string, breaking NoTxWrap migrations (e.g. "BEGIN; ...; COMMIT;").
27
// execerContextConn and execerContextDriver promote Execer → ExecerContext so
28
// that otelsql can wrap the connection while preserving multi-statement support.
29

30
type execerContextConn struct {
31
        driver.Conn
32
        execer driver.Execer //nolint:staticcheck
33
}
34

35
func (c *execerContextConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
60✔
36
        dargs := make([]driver.Value, len(args))
60✔
37
        for i, nv := range args {
100✔
38
                dargs[i] = nv.Value
40✔
39
        }
40✔
40
        return c.execer.Exec(query, dargs)
60✔
41
}
42

43
type execerContextDriver struct {
44
        driver.Driver
45
}
46

47
func (d *execerContextDriver) Open(name string) (driver.Conn, error) {
4✔
48
        conn, err := d.Driver.Open(name)
4✔
49
        if err != nil {
4✔
NEW
50
                return nil, err
×
NEW
51
        }
×
52
        if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck
8✔
53
                return &execerContextConn{Conn: conn, execer: execer}, nil
4✔
54
        }
4✔
NEW
55
        return conn, nil
×
56
}
57

58
const sqlcipherWrappedDriver = "sqlite3-sqlcipher-otel"
59

60
var registerDriverOnce sync.Once
61

62
// wrappedSQLCipherDriverName returns the name of the go-sqlcipher driver wrapped
63
// with execerContextDriver, registering it on the first call.
64
func wrappedSQLCipherDriverName() string {
6✔
65
        registerDriverOnce.Do(func() {
8✔
66
                // sql.Open is lazy; it does not open a connection, just resolves the driver.
2✔
67
                db, _ := sql.Open("sqlite3", ":memory:")
2✔
68
                drv := db.Driver()
2✔
69
                _ = db.Close()
2✔
70
                sql.Register(sqlcipherWrappedDriver, &execerContextDriver{drv})
2✔
71
        })
2✔
72
        return sqlcipherWrappedDriver
6✔
73
}
74

75
func init() {
2✔
76
        database.Register("sqlcipher", &Sqlite{})
2✔
77
}
2✔
78

79
var DefaultMigrationsTable = "schema_migrations"
80
var (
81
        ErrDatabaseDirty  = fmt.Errorf("database is dirty")
82
        ErrNilConfig      = fmt.Errorf("no config")
83
        ErrNoDatabaseName = fmt.Errorf("no database name")
84
)
85

86
type Config struct {
87
        MigrationsTable string
88
        DatabaseName    string
89
        NoTxWrap        bool
90
}
91

92
type Sqlite struct {
93
        db       *sql.DB
94
        isLocked atomic.Bool
95

96
        config *Config
97
}
98

99
func WithInstance(ctx context.Context, instance *sql.DB, config *Config) (database.Driver, error) {
8✔
100
        if config == nil {
8✔
101
                return nil, ErrNilConfig
×
102
        }
×
103

104
        if err := instance.Ping(); err != nil {
8✔
105
                return nil, err
×
106
        }
×
107

108
        if len(config.MigrationsTable) == 0 {
10✔
109
                config.MigrationsTable = DefaultMigrationsTable
2✔
110
        }
2✔
111

112
        mx := &Sqlite{
8✔
113
                db:     instance,
8✔
114
                config: config,
8✔
115
        }
8✔
116
        if err := mx.ensureVersionTable(ctx); err != nil {
8✔
117
                return nil, err
×
118
        }
×
119
        return mx, nil
8✔
120
}
121

122
// ensureVersionTable checks if versions table exists and, if not, creates it.
123
// Note that this function locks the database, which deviates from the usual
124
// convention of "caller locks" in the Sqlite type.
125
func (m *Sqlite) ensureVersionTable(ctx context.Context) (err error) {
8✔
126
        if err = m.Lock(ctx); err != nil {
8✔
127
                return err
×
128
        }
×
129

130
        defer func() {
16✔
131
                if e := m.Unlock(ctx); e != nil {
8✔
132
                        err = errors.Join(err, e)
×
133
                }
×
134
        }()
135

136
        query := fmt.Sprintf(`
8✔
137
        CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
8✔
138
  CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
8✔
139
  `, m.config.MigrationsTable, m.config.MigrationsTable)
8✔
140

8✔
141
        if _, err := m.db.ExecContext(ctx, query); err != nil {
8✔
142
                return err
×
143
        }
×
144
        return nil
8✔
145
}
146

147
func (m *Sqlite) Open(ctx context.Context, url string) (database.Driver, error) {
6✔
148
        purl, err := nurl.Parse(url)
6✔
149
        if err != nil {
6✔
150
                return nil, err
×
151
        }
×
152
        dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "sqlite3://", "", 1)
6✔
153
        db, err := otelsql.Open(wrappedSQLCipherDriverName(), dbfile,
6✔
154
                otelsql.WithAttributes(semconv.DBSystemNameSQLite),
6✔
155
        )
6✔
156
        if err != nil {
6✔
157
                return nil, err
×
158
        }
×
159

160
        qv := purl.Query()
6✔
161

6✔
162
        migrationsTable := qv.Get("x-migrations-table")
6✔
163
        if len(migrationsTable) == 0 {
12✔
164
                migrationsTable = DefaultMigrationsTable
6✔
165
        }
6✔
166

167
        noTxWrap := false
6✔
168
        if v := qv.Get("x-no-tx-wrap"); v != "" {
10✔
169
                noTxWrap, err = strconv.ParseBool(v)
4✔
170
                if err != nil {
6✔
171
                        return nil, fmt.Errorf("x-no-tx-wrap: %s", err)
2✔
172
                }
2✔
173
        }
174

175
        mx, err := WithInstance(ctx, db, &Config{
4✔
176
                DatabaseName:    purl.Path,
4✔
177
                MigrationsTable: migrationsTable,
4✔
178
                NoTxWrap:        noTxWrap,
4✔
179
        })
4✔
180
        if err != nil {
4✔
181
                return nil, err
×
182
        }
×
183
        return mx, nil
4✔
184
}
185

NEW
186
func (m *Sqlite) Close(ctx context.Context) error {
×
187
        return m.db.Close()
×
188
}
×
189

190
func (m *Sqlite) Drop(ctx context.Context) (err error) {
6✔
191
        query := `SELECT name FROM sqlite_master WHERE type = 'table';`
6✔
192
        tables, err := m.db.QueryContext(ctx, query)
6✔
193
        if err != nil {
6✔
194
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
195
        }
×
196
        defer func() {
12✔
197
                if errClose := tables.Close(); errClose != nil {
6✔
198
                        err = errors.Join(err, errClose)
×
199
                }
×
200
        }()
201

202
        tableNames := make([]string, 0)
6✔
203
        for tables.Next() {
18✔
204
                var tableName string
12✔
205
                if err := tables.Scan(&tableName); err != nil {
12✔
206
                        return err
×
207
                }
×
208
                if len(tableName) > 0 {
24✔
209
                        tableNames = append(tableNames, tableName)
12✔
210
                }
12✔
211
        }
212
        if err := tables.Err(); err != nil {
6✔
213
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
214
        }
×
215

216
        if len(tableNames) > 0 {
12✔
217
                for _, t := range tableNames {
18✔
218
                        query := "DROP TABLE " + t
12✔
219
                        err = m.executeQuery(ctx, query)
12✔
220
                        if err != nil {
12✔
221
                                return &database.Error{OrigErr: err, Query: []byte(query)}
×
222
                        }
×
223
                }
224
                query := "VACUUM"
6✔
225
                _, err = m.db.QueryContext(ctx, query)
6✔
226
                if err != nil {
6✔
227
                        return &database.Error{OrigErr: err, Query: []byte(query)}
×
228
                }
×
229
        }
230

231
        return nil
6✔
232
}
233

234
func (m *Sqlite) Lock(ctx context.Context) error {
26✔
235
        if !m.isLocked.CompareAndSwap(false, true) {
30✔
236
                return database.ErrLocked
4✔
237
        }
4✔
238
        return nil
22✔
239
}
240

241
func (m *Sqlite) Unlock(ctx context.Context) error {
22✔
242
        if !m.isLocked.CompareAndSwap(true, false) {
22✔
243
                return database.ErrNotLocked
×
244
        }
×
245
        return nil
22✔
246
}
247

248
func (m *Sqlite) Run(ctx context.Context, migration io.Reader) error {
12✔
249
        migr, err := io.ReadAll(migration)
12✔
250
        if err != nil {
12✔
251
                return err
×
252
        }
×
253
        query := string(migr[:])
12✔
254

12✔
255
        if m.config.NoTxWrap {
14✔
256
                return m.executeQueryNoTx(ctx, query)
2✔
257
        }
2✔
258
        return m.executeQuery(ctx, query)
10✔
259
}
260

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

278
func (m *Sqlite) executeQueryNoTx(ctx context.Context, query string) error {
2✔
279
        if _, err := m.db.ExecContext(ctx, 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(ctx context.Context, version int, dirty bool) error {
40✔
286
        tx, err := m.db.BeginTx(ctx, nil)
40✔
287
        if err != nil {
40✔
288
                return &database.Error{OrigErr: err, Err: "transaction start failed"}
×
289
        }
×
290

291
        query := "DELETE FROM " + m.config.MigrationsTable
40✔
292
        if _, err := tx.ExecContext(ctx, query); err != nil {
40✔
293
                return &database.Error{OrigErr: err, Query: []byte(query)}
×
294
        }
×
295

296
        // Also re-write the schema version for nil dirty versions to prevent
297
        // empty schema version for failed down migration on the first migration
298
        // See: https://github.com/golang-migrate/migrate/issues/330
299
        if version >= 0 || (version == database.NilVersion && dirty) {
76✔
300
                query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
36✔
301
                if _, err := tx.ExecContext(ctx, query, version, dirty); err != nil {
36✔
302
                        if errRollback := tx.Rollback(); errRollback != nil {
×
303
                                err = errors.Join(err, errRollback)
×
304
                        }
×
305
                        return &database.Error{OrigErr: err, Query: []byte(query)}
×
306
                }
307
        }
308

309
        if err := tx.Commit(); err != nil {
40✔
310
                return &database.Error{OrigErr: err, Err: "transaction commit failed"}
×
311
        }
×
312

313
        return nil
40✔
314
}
315

316
func (m *Sqlite) Version(ctx context.Context) (version int, dirty bool, err error) {
32✔
317
        query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
32✔
318
        err = m.db.QueryRowContext(ctx, query).Scan(&version, &dirty)
32✔
319
        if err != nil {
44✔
320
                return database.NilVersion, false, nil
12✔
321
        }
12✔
322
        return version, dirty, nil
20✔
323
}
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

© 2026 Coveralls, Inc