• 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

67.63
/database/spanner/spanner.go
1
package spanner
2

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

14
        "cloud.google.com/go/spanner"
15
        sdb "cloud.google.com/go/spanner/admin/database/apiv1"
16
        "cloud.google.com/go/spanner/spansql"
17

18
        "github.com/golang-migrate/migrate/v4"
19
        "github.com/golang-migrate/migrate/v4/database"
20

21
        adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
22
        "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
23
        "google.golang.org/api/iterator"
24
        "google.golang.org/api/option"
25
        "google.golang.org/grpc"
26
)
27

28
func init() {
2✔
29
        db := Spanner{}
2✔
30
        database.Register("spanner", &db)
2✔
31
}
2✔
32

33
// DefaultMigrationsTable is used if no custom table is specified
34
const DefaultMigrationsTable = "SchemaMigrations"
35

36
// Driver errors
37
var (
38
        ErrNilConfig      = errors.New("no config")
39
        ErrNoDatabaseName = errors.New("no database name")
40
        ErrNoSchema       = errors.New("no schema")
41
        ErrDatabaseDirty  = errors.New("database is dirty")
42
        ErrLockHeld       = errors.New("unable to obtain lock")
43
        ErrLockNotHeld    = errors.New("unable to release already released lock")
44
)
45

46
// Config used for a Spanner instance
47
type Config struct {
48
        MigrationsTable string
49
        DatabaseName    string
50
        // Whether to parse the migration DDL with spansql before
51
        // running them towards Spanner.
52
        // Parsing outputs clean DDL statements such as reformatted
53
        // and void of comments.
54
        CleanStatements bool
55
}
56

57
// Spanner implements database.Driver for Google Cloud Spanner
58
type Spanner struct {
59
        db *DB
60

61
        config *Config
62

63
        lock atomic.Bool
64
}
65

66
type DB struct {
67
        admin *sdb.DatabaseAdminClient
68
        data  *spanner.Client
69
}
70

71
func NewDB(admin sdb.DatabaseAdminClient, data spanner.Client) *DB {
×
72
        return &DB{
×
73
                admin: &admin,
×
74
                data:  &data,
×
75
        }
×
76
}
×
77

78
// WithInstance implements database.Driver
79
func WithInstance(ctx context.Context, instance *DB, config *Config) (database.Driver, error) {
4✔
80
        if config == nil {
4✔
81
                return nil, ErrNilConfig
×
82
        }
×
83

84
        if len(config.DatabaseName) == 0 {
4✔
85
                return nil, ErrNoDatabaseName
×
86
        }
×
87

88
        if len(config.MigrationsTable) == 0 {
8✔
89
                config.MigrationsTable = DefaultMigrationsTable
4✔
90
        }
4✔
91

92
        sx := &Spanner{
4✔
93
                db:     instance,
4✔
94
                config: config,
4✔
95
        }
4✔
96

4✔
97
        if err := sx.ensureVersionTable(ctx); err != nil {
4✔
98
                return nil, err
×
99
        }
×
100

101
        return sx, nil
4✔
102
}
103

104
// Open implements database.Driver
105
func (s *Spanner) Open(ctx context.Context, url string) (database.Driver, error) {
4✔
106
        purl, err := nurl.Parse(url)
4✔
107
        if err != nil {
4✔
108
                return nil, err
×
109
        }
×
110

111
        adminClient, err := sdb.NewDatabaseAdminClient(ctx, option.WithGRPCDialOption(grpc.WithStatsHandler(otelgrpc.NewClientHandler())))
4✔
112
        if err != nil {
4✔
113
                return nil, err
×
114
        }
×
115
        dbname := strings.Replace(migrate.FilterCustomQuery(purl).String(), "spanner://", "", 1)
4✔
116
        dataClient, err := spanner.NewClient(ctx, dbname, option.WithGRPCDialOption(grpc.WithStatsHandler(otelgrpc.NewClientHandler())))
4✔
117
        if err != nil {
4✔
NEW
118
                _ = adminClient.Close()
×
NEW
119
                return nil, err
×
UNCOV
120
        }
×
121

122
        migrationsTable := purl.Query().Get("x-migrations-table")
4✔
123

4✔
124
        cleanQuery := purl.Query().Get("x-clean-statements")
4✔
125
        clean := false
4✔
126
        if cleanQuery != "" {
4✔
127
                clean, err = strconv.ParseBool(cleanQuery)
×
128
                if err != nil {
×
129
                        return nil, err
×
130
                }
×
131
        }
132

133
        db := &DB{admin: adminClient, data: dataClient}
4✔
134
        return WithInstance(ctx, db, &Config{
4✔
135
                DatabaseName:    dbname,
4✔
136
                MigrationsTable: migrationsTable,
4✔
137
                CleanStatements: clean,
4✔
138
        })
4✔
139
}
140

141
// Close implements database.Driver
NEW
142
func (s *Spanner) Close(ctx context.Context) error {
×
143
        s.db.data.Close()
×
144
        return s.db.admin.Close()
×
145
}
×
146

147
// Lock implements database.Driver but doesn't do anything because Spanner only
148
// enqueues the UpdateDatabaseDdlRequest.
149
func (s *Spanner) Lock(ctx context.Context) error {
14✔
150
        if swapped := s.lock.CompareAndSwap(false, true); swapped {
26✔
151
                return nil
12✔
152
        }
12✔
153
        return ErrLockHeld
2✔
154
}
155

156
// Unlock implements database.Driver but no action required, see Lock.
157
func (s *Spanner) Unlock(ctx context.Context) error {
12✔
158
        if swapped := s.lock.CompareAndSwap(true, false); swapped {
24✔
159
                return nil
12✔
160
        }
12✔
161
        return ErrLockNotHeld
×
162
}
163

164
// Run implements database.Driver
165
func (s *Spanner) Run(ctx context.Context, migration io.Reader) error {
12✔
166
        migr, err := io.ReadAll(migration)
12✔
167
        if err != nil {
12✔
168
                return err
×
169
        }
×
170

171
        stmts := []string{string(migr)}
12✔
172
        if s.config.CleanStatements {
12✔
173
                stmts, err = cleanStatements(migr)
×
174
                if err != nil {
×
175
                        return err
×
176
                }
×
177
        }
178

179
        op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
12✔
180
                Database:   s.config.DatabaseName,
12✔
181
                Statements: stmts,
12✔
182
        })
12✔
183

12✔
184
        if err != nil {
12✔
185
                return &database.Error{OrigErr: err, Err: "migration failed", Query: migr}
×
186
        }
×
187

188
        if err := op.Wait(ctx); err != nil {
12✔
189
                return &database.Error{OrigErr: err, Err: "migration failed", Query: migr}
×
190
        }
×
191

192
        return nil
12✔
193
}
194

195
// SetVersion implements database.Driver
196
func (s *Spanner) SetVersion(ctx context.Context, version int, dirty bool) error {
32✔
197
        _, err := s.db.data.ReadWriteTransaction(ctx,
32✔
198
                func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
64✔
199
                        m := []*spanner.Mutation{
32✔
200
                                spanner.Delete(s.config.MigrationsTable, spanner.AllKeys()),
32✔
201
                                spanner.Insert(s.config.MigrationsTable,
32✔
202
                                        []string{"Version", "Dirty"},
32✔
203
                                        []interface{}{version, dirty},
32✔
204
                                )}
32✔
205
                        return txn.BufferWrite(m)
32✔
206
                })
32✔
207
        if err != nil {
32✔
208
                return &database.Error{OrigErr: err}
×
209
        }
×
210

211
        return nil
32✔
212
}
213

214
// Version implements database.Driver
215
func (s *Spanner) Version(ctx context.Context) (version int, dirty bool, err error) {
16✔
216
        stmt := spanner.Statement{
16✔
217
                SQL: `SELECT Version, Dirty FROM ` + s.config.MigrationsTable + ` LIMIT 1`,
16✔
218
        }
16✔
219
        iter := s.db.data.Single().Query(ctx, stmt)
16✔
220
        defer iter.Stop()
16✔
221

16✔
222
        row, err := iter.Next()
16✔
223
        switch err {
16✔
224
        case iterator.Done:
4✔
225
                return database.NilVersion, false, nil
4✔
226
        case nil:
12✔
227
                var v int64
12✔
228
                if err = row.Columns(&v, &dirty); err != nil {
12✔
229
                        return 0, false, &database.Error{OrigErr: err, Query: []byte(stmt.SQL)}
×
230
                }
×
231
                version = int(v)
12✔
232
        default:
×
233
                return 0, false, &database.Error{OrigErr: err, Query: []byte(stmt.SQL)}
×
234
        }
235

236
        return version, dirty, nil
12✔
237
}
238

239
var nameMatcher = regexp.MustCompile(`(CREATE TABLE\s(\S+)\s)|(CREATE.+INDEX\s(\S+)\s)`)
240

241
// Drop implements database.Driver. Retrieves the database schema first and
242
// creates statements to drop the indexes and tables accordingly.
243
// Note: The drop statements are created in reverse order to how they're
244
// provided in the schema. Assuming the schema describes how the database can
245
// be "build up", it seems logical to "unbuild" the database simply by going the
246
// opposite direction. More testing
247
func (s *Spanner) Drop(ctx context.Context) error {
4✔
248
        res, err := s.db.admin.GetDatabaseDdl(ctx, &adminpb.GetDatabaseDdlRequest{
4✔
249
                Database: s.config.DatabaseName,
4✔
250
        })
4✔
251
        if err != nil {
4✔
252
                return &database.Error{OrigErr: err, Err: "drop failed"}
×
253
        }
×
254
        if len(res.Statements) == 0 {
4✔
255
                return nil
×
256
        }
×
257

258
        stmts := make([]string, 0)
4✔
259
        for i := len(res.Statements) - 1; i >= 0; i-- {
16✔
260
                s := res.Statements[i]
12✔
261
                m := nameMatcher.FindSubmatch([]byte(s))
12✔
262

12✔
263
                if len(m) == 0 {
12✔
264
                        continue
×
265
                } else if tbl := m[2]; len(tbl) > 0 {
24✔
266
                        stmts = append(stmts, fmt.Sprintf(`DROP TABLE %s`, tbl))
12✔
267
                } else if idx := m[4]; len(idx) > 0 {
12✔
268
                        stmts = append(stmts, fmt.Sprintf(`DROP INDEX %s`, idx))
×
269
                }
×
270
        }
271

272
        op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
4✔
273
                Database:   s.config.DatabaseName,
4✔
274
                Statements: stmts,
4✔
275
        })
4✔
276
        if err != nil {
4✔
277
                return &database.Error{OrigErr: err, Query: []byte(strings.Join(stmts, "; "))}
×
278
        }
×
279
        if err := op.Wait(ctx); err != nil {
4✔
280
                return &database.Error{OrigErr: err, Query: []byte(strings.Join(stmts, "; "))}
×
281
        }
×
282

283
        return nil
4✔
284
}
285

286
// ensureVersionTable checks if versions table exists and, if not, creates it.
287
// Note that this function locks the database, which deviates from the usual
288
// convention of "caller locks" in the Spanner type.
289
func (s *Spanner) ensureVersionTable(ctx context.Context) (err error) {
4✔
290
        if err = s.Lock(ctx); err != nil {
4✔
291
                return err
×
292
        }
×
293

294
        defer func() {
8✔
295
                if e := s.Unlock(ctx); e != nil {
4✔
296
                        err = errors.Join(err, e)
×
297
                }
×
298
        }()
299

300
        tbl := s.config.MigrationsTable
4✔
301
        iter := s.db.data.Single().Read(ctx, tbl, spanner.AllKeys(), []string{"Version"})
4✔
302
        if err := iter.Do(func(r *spanner.Row) error { return nil }); err == nil {
4✔
303
                return nil
×
304
        }
×
305

306
        stmt := fmt.Sprintf(`CREATE TABLE %s (
4✔
307
    Version INT64 NOT NULL,
4✔
308
    Dirty    BOOL NOT NULL
4✔
309
        ) PRIMARY KEY(Version)`, tbl)
4✔
310

4✔
311
        op, err := s.db.admin.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
4✔
312
                Database:   s.config.DatabaseName,
4✔
313
                Statements: []string{stmt},
4✔
314
        })
4✔
315

4✔
316
        if err != nil {
4✔
317
                return &database.Error{OrigErr: err, Query: []byte(stmt)}
×
318
        }
×
319
        if err := op.Wait(ctx); err != nil {
4✔
320
                return &database.Error{OrigErr: err, Query: []byte(stmt)}
×
321
        }
×
322

323
        return nil
4✔
324
}
325

326
func cleanStatements(migration []byte) ([]string, error) {
22✔
327
        // The Spanner GCP backend does not yet support comments for the UpdateDatabaseDdl RPC
22✔
328
        // (see https://issuetracker.google.com/issues/159730604) we use
22✔
329
        // spansql to parse the DDL and output valid stamements without comments
22✔
330
        ddl, err := spansql.ParseDDL("", string(migration))
22✔
331
        if err != nil {
22✔
332
                return nil, err
×
333
        }
×
334
        stmts := make([]string, 0, len(ddl.List))
22✔
335
        for _, stmt := range ddl.List {
50✔
336
                stmts = append(stmts, stmt.SQL())
28✔
337
        }
28✔
338
        return stmts, nil
22✔
339
}
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