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

stephenafamo / bob / 15009121170

14 May 2025 12:01AM UTC coverage: 36.698% (+0.07%) from 36.632%
15009121170

Pull #416

github

stephenafamo
Add support for MySQL to `bobgen-sql`
Pull Request #416: Add support for MySQL to `bobgen-sql`

31 of 48 new or added lines in 2 files covered. (64.58%)

2 existing lines in 1 file now uncovered.

6536 of 17810 relevant lines covered (36.7%)

233.69 hits per line

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

70.19
/gen/bobgen-sql/driver/sql.go
1
package driver
2

3
import (
4
        "context"
5
        "database/sql"
6
        "fmt"
7
        "io/fs"
8
        "os"
9
        "path/filepath"
10

11
        embeddedpostgres "github.com/fergusstrange/embedded-postgres"
12
        "github.com/lib/pq"
13
        "github.com/stephenafamo/bob/gen"
14
        helpers "github.com/stephenafamo/bob/gen/bobgen-helpers"
15
        mysqlDriver "github.com/stephenafamo/bob/gen/bobgen-mysql/driver"
16
        psqlDriver "github.com/stephenafamo/bob/gen/bobgen-psql/driver"
17
        sqliteDriver "github.com/stephenafamo/bob/gen/bobgen-sqlite/driver"
18
        "github.com/stephenafamo/bob/gen/drivers"
19
        "github.com/testcontainers/testcontainers-go"
20
        mysqltest "github.com/testcontainers/testcontainers-go/modules/mysql"
21
)
22

23
type Config struct {
24
        // What dialect to generate with
25
        // psql | mysql | sqlite
26
        Dialect string
27
        // Glob pattern to match migration files
28
        Pattern string
29
        // Folders containing query files
30
        Queries []string `yaml:"queries"`
31
        // The database schemas to generate models for
32
        Schemas []string
33
        // The name of this schema will not be included in the generated models
34
        // a context value can then be used to set the schema at runtime
35
        // useful for multi-tenant setups
36
        SharedSchema string `yaml:"shared_schema"`
37
        // List of tables that will be included. Others are ignored
38
        Only map[string][]string
39
        // List of tables that will be should be ignored. Others are included
40
        Except map[string][]string
41
        // How many tables to fetch in parallel
42
        Concurrency int
43
        // Which UUID package to use (gofrs or google)
44
        UUIDPkg string `yaml:"uuid_pkg"`
45
        // Which `database/sql` driver to use (the full module name)
46
        DriverName string `yaml:"driver_name"`
47

48
        Output    string
49
        Pkgname   string
50
        NoFactory bool `yaml:"no_factory"`
51

52
        fs fs.FS
53
}
54

55
func RunPostgres(ctx context.Context, state *gen.State[any], config Config) error {
×
56
        d, err := getPsqlDriver(ctx, config)
×
57
        if err != nil {
×
58
                return fmt.Errorf("getting psql driver: %w", err)
×
59
        }
×
60

61
        return gen.Run(ctx, state, d)
×
62
}
63

64
func getPsqlDriver(ctx context.Context, config Config) (psqlDriver.Interface, error) {
2✔
65
        port, err := helpers.GetFreePort()
2✔
66
        if err != nil {
2✔
67
                return nil, fmt.Errorf("could not get a free port: %w", err)
×
68
        }
×
69

70
        dbConfig := embeddedpostgres.
2✔
71
                DefaultConfig().
2✔
72
                RuntimePath(filepath.Join(os.TempDir(), "bobgen_sql")).
2✔
73
                Port(uint32(port))
2✔
74
        dsn := dbConfig.GetConnectionURL() + "?sslmode=disable"
2✔
75

2✔
76
        postgres := embeddedpostgres.NewDatabase(dbConfig)
2✔
77
        if err := postgres.Start(); err != nil {
2✔
78
                return nil, fmt.Errorf("starting embedded postgres: %w", err)
×
79
        }
×
80
        defer func() {
4✔
81
                if err := postgres.Stop(); err != nil {
2✔
82
                        fmt.Println("Error stopping postgres:", err)
×
83
                }
×
84
        }()
85

86
        db, err := sql.Open("postgres", dsn)
2✔
87
        if err != nil {
2✔
88
                return nil, fmt.Errorf("failed to connect to database: %w", err)
×
89
        }
×
90
        defer db.Close()
2✔
91

2✔
92
        if err := helpers.Migrate(ctx, db, config.fs, config.Pattern); err != nil {
2✔
93
                return nil, fmt.Errorf("migrating: %w", err)
×
94
        }
×
95
        db.Close() // close early
2✔
96

2✔
97
        d := wrapDriver(ctx, psqlDriver.New(psqlDriver.Config{
2✔
98
                Dsn: dsn,
2✔
99

2✔
100
                Schemas:      pq.StringArray(config.Schemas),
2✔
101
                SharedSchema: config.SharedSchema,
2✔
102
                Queries:      config.Queries,
2✔
103
                Only:         config.Only,
2✔
104
                Except:       config.Except,
2✔
105
                Concurrency:  config.Concurrency,
2✔
106
                UUIDPkg:      config.UUIDPkg,
2✔
107
                DriverName:   config.DriverName,
2✔
108
                Output:       config.Output,
2✔
109
                Pkgname:      config.Pkgname,
2✔
110
                NoFactory:    config.NoFactory,
2✔
111
        }))
2✔
112

2✔
113
        return d, nil
2✔
114
}
115

NEW
116
func RunMySQL(ctx context.Context, state *gen.State[any], config Config) error {
×
NEW
117
        d, err := getMySQLDriver(ctx, config)
×
NEW
118
        if err != nil {
×
NEW
119
                return fmt.Errorf("getting mysql driver: %w", err)
×
NEW
120
        }
×
121

NEW
122
        return gen.Run(ctx, state, d)
×
123
}
124

125
func getMySQLDriver(ctx context.Context, config Config) (mysqlDriver.Interface, error) {
2✔
126
        mysqlContainer, err := mysqltest.Run(ctx,
2✔
127
                "mysql:8.0.35",
2✔
128
                mysqltest.WithDatabase("bobgen"),
2✔
129
                mysqltest.WithUsername("root"),
2✔
130
                mysqltest.WithPassword("password"),
2✔
131
        )
2✔
132
        defer func() {
4✔
133
                if err := testcontainers.TerminateContainer(mysqlContainer); err != nil {
2✔
NEW
134
                        fmt.Printf("failed to terminate MySQL container: %v\n", err)
×
NEW
135
                }
×
136
        }()
137
        if err != nil {
2✔
NEW
138
                return nil, fmt.Errorf("failed to start container: %w", err)
×
NEW
139
        }
×
140

141
        dsn, err := mysqlContainer.ConnectionString(ctx, "tls=skip-verify", "multiStatements=true")
2✔
142
        if err != nil {
2✔
NEW
143
                return nil, fmt.Errorf("failed to get connection string: %w", err)
×
NEW
144
        }
×
145

146
        db, err := sql.Open("mysql", dsn)
2✔
147
        if err != nil {
2✔
NEW
148
                return nil, fmt.Errorf("failed to connect to database: %w", err)
×
NEW
149
        }
×
150
        defer db.Close()
2✔
151

2✔
152
        if err := helpers.Migrate(ctx, db, config.fs, config.Pattern); err != nil {
2✔
NEW
153
                return nil, fmt.Errorf("migrating: %w", err)
×
NEW
154
        }
×
155
        db.Close() // close early
2✔
156

2✔
157
        d := wrapDriver(ctx, mysqlDriver.New(mysqlDriver.Config{
2✔
158
                Dsn: dsn,
2✔
159

2✔
160
                Only:        config.Only,
2✔
161
                Except:      config.Except,
2✔
162
                Concurrency: config.Concurrency,
2✔
163
                Output:      config.Output,
2✔
164
                Pkgname:     config.Pkgname,
2✔
165
                NoFactory:   config.NoFactory,
2✔
166
        }))
2✔
167

2✔
168
        return d, nil
2✔
169
}
170

171
func RunSQLite(ctx context.Context, state *gen.State[any], config Config) error {
×
172
        d, err := getSQLiteDriver(ctx, config)
×
173
        if err != nil {
×
174
                return fmt.Errorf("getting sqlite driver: %w", err)
×
175
        }
×
176

177
        return gen.Run(ctx, state, d)
×
178
}
179

180
func getSQLiteDriver(ctx context.Context, config Config) (sqliteDriver.Interface, error) {
2✔
181
        tmp, err := os.CreateTemp("", "bobgen_sqlite")
2✔
182
        if err != nil {
2✔
183
                return nil, fmt.Errorf("creating temp file: %w", err)
×
184
        }
×
185
        defer tmp.Close()
2✔
186

2✔
187
        db, err := sql.Open("sqlite", tmp.Name())
2✔
188
        if err != nil {
2✔
189
                return nil, fmt.Errorf("failed to connect to database: %w", err)
×
190
        }
×
191
        defer db.Close()
2✔
192

2✔
193
        attach := make(map[string]string)
2✔
194
        for _, schema := range config.Schemas {
4✔
195
                tmp, err := os.CreateTemp("", "bobgen_sqlite_"+schema)
2✔
196
                if err != nil {
2✔
197
                        return nil, fmt.Errorf("creating temp file: %w", err)
×
198
                }
×
199
                defer tmp.Close()
2✔
200

2✔
201
                attach[schema] = tmp.Name()
2✔
202
                _, err = db.ExecContext(ctx, fmt.Sprintf(
2✔
203
                        "attach database '%s' as %s", tmp.Name(), schema,
2✔
204
                ))
2✔
205
                if err != nil {
2✔
206
                        return nil, fmt.Errorf("could not attach %q: %w", schema, err)
×
207
                }
×
208
        }
209

210
        if err := helpers.Migrate(ctx, db, config.fs, config.Pattern); err != nil {
2✔
211
                return nil, fmt.Errorf("migrating: %w", err)
×
212
        }
×
213
        db.Close() // close early
2✔
214

2✔
215
        d := sqliteDriver.New(sqliteDriver.Config{
2✔
216
                DSN:        tmp.Name(),
2✔
217
                Attach:     attach,
2✔
218
                Queries:    config.Queries,
2✔
219
                DriverName: config.DriverName,
2✔
220

2✔
221
                SharedSchema: config.SharedSchema,
2✔
222
                Only:         config.Only,
2✔
223
                Except:       config.Except,
2✔
224
                Output:       config.Output,
2✔
225
                Pkgname:      config.Pkgname,
2✔
226
                NoFactory:    config.NoFactory,
2✔
227
        })
2✔
228

2✔
229
        return d, nil
2✔
230
}
231

232
func wrapDriver[T, C, I any](ctx context.Context, d drivers.Interface[T, C, I]) driver[T, C, I] {
4✔
233
        info, err := d.Assemble(ctx)
4✔
234
        return driver[T, C, I]{d, info, err}
4✔
235
}
4✔
236

237
type driver[T, C, I any] struct {
238
        drivers.Interface[T, C, I]
239
        info *drivers.DBInfo[T, C, I]
240
        err  error
241
}
242

243
func (d driver[T, C, I]) Assemble(context.Context) (*drivers.DBInfo[T, C, I], error) {
4✔
244
        return d.info, d.err
4✔
245
}
4✔
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