Skip to content

Commit 42cf605

Browse files
committed
feat(mssql): add MSSQL support to end-to-end tests
Add native and Docker support for starting MSSQL Server instances in end-to-end tests, following the same pattern as PostgreSQL and MySQL. - Add internal/sqltest/native/mssql.go for native MSSQL service - Add internal/sqltest/docker/mssql.go for Docker-based MSSQL - Update endtoend_test.go to initialize and use MSSQL connections 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a468bb1 commit 42cf605

File tree

3 files changed

+270
-4
lines changed

3 files changed

+270
-4
lines changed

internal/endtoend/endtoend_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func TestReplay(t *testing.T) {
113113
// t.Parallel()
114114
ctx := context.Background()
115115

116-
var mysqlURI, postgresURI string
116+
var mysqlURI, postgresURI, mssqlURI string
117117

118118
// First, check environment variables
119119
if uri := os.Getenv("POSTGRESQL_SERVER_URI"); uri != "" {
@@ -122,9 +122,12 @@ func TestReplay(t *testing.T) {
122122
if uri := os.Getenv("MYSQL_SERVER_URI"); uri != "" {
123123
mysqlURI = uri
124124
}
125+
if uri := os.Getenv("MSSQL_SERVER_URI"); uri != "" {
126+
mssqlURI = uri
127+
}
125128

126129
// Try Docker for any missing databases
127-
if postgresURI == "" || mysqlURI == "" {
130+
if postgresURI == "" || mysqlURI == "" || mssqlURI == "" {
128131
if err := docker.Installed(); err == nil {
129132
if postgresURI == "" {
130133
host, err := docker.StartPostgreSQLServer(ctx)
@@ -142,11 +145,19 @@ func TestReplay(t *testing.T) {
142145
mysqlURI = host
143146
}
144147
}
148+
if mssqlURI == "" {
149+
host, err := docker.StartMSSQLServer(ctx)
150+
if err != nil {
151+
t.Logf("docker mssql startup failed: %s", err)
152+
} else {
153+
mssqlURI = host
154+
}
155+
}
145156
}
146157
}
147158

148159
// Try native installation for any missing databases (Linux only)
149-
if postgresURI == "" || mysqlURI == "" {
160+
if postgresURI == "" || mysqlURI == "" || mssqlURI == "" {
150161
if err := native.Supported(); err == nil {
151162
if postgresURI == "" {
152163
host, err := native.StartPostgreSQLServer(ctx)
@@ -164,12 +175,21 @@ func TestReplay(t *testing.T) {
164175
mysqlURI = host
165176
}
166177
}
178+
if mssqlURI == "" {
179+
host, err := native.StartMSSQLServer(ctx)
180+
if err != nil {
181+
t.Logf("native mssql startup failed: %s", err)
182+
} else {
183+
mssqlURI = host
184+
}
185+
}
167186
}
168187
}
169188

170189
// Log which databases are available
171190
t.Logf("PostgreSQL available: %v (URI: %s)", postgresURI != "", postgresURI)
172191
t.Logf("MySQL available: %v (URI: %s)", mysqlURI != "", mysqlURI)
192+
t.Logf("MSSQL available: %v (URI: %s)", mssqlURI != "", mssqlURI)
173193

174194
contexts := map[string]textContext{
175195
"base": {
@@ -191,6 +211,11 @@ func TestReplay(t *testing.T) {
191211
Engine: config.EngineMySQL,
192212
URI: mysqlURI,
193213
},
214+
{
215+
Name: "mssql",
216+
Engine: config.EngineMSSQL,
217+
URI: mssqlURI,
218+
},
194219
}
195220

196221
for i := range c.SQL {
@@ -207,6 +232,10 @@ func TestReplay(t *testing.T) {
207232
c.SQL[i].Database = &config.Database{
208233
Managed: true,
209234
}
235+
case config.EngineMSSQL:
236+
c.SQL[i].Database = &config.Database{
237+
Managed: true,
238+
}
210239
default:
211240
// pass
212241
}
@@ -215,7 +244,7 @@ func TestReplay(t *testing.T) {
215244
},
216245
Enabled: func() bool {
217246
// Enabled if at least one database URI is available
218-
return postgresURI != "" || mysqlURI != ""
247+
return postgresURI != "" || mysqlURI != "" || mssqlURI != ""
219248
},
220249
},
221250
}

internal/sqltest/docker/mssql.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"os/exec"
8+
"strings"
9+
"time"
10+
11+
_ "github.com/microsoft/go-mssqldb"
12+
)
13+
14+
var mssqlHost string
15+
16+
func StartMSSQLServer(c context.Context) (string, error) {
17+
if err := Installed(); err != nil {
18+
return "", err
19+
}
20+
if mssqlHost != "" {
21+
return mssqlHost, nil
22+
}
23+
value, err, _ := flight.Do("mssql", func() (interface{}, error) {
24+
host, err := startMSSQLServer(c)
25+
if err != nil {
26+
return "", err
27+
}
28+
mssqlHost = host
29+
return host, nil
30+
})
31+
if err != nil {
32+
return "", err
33+
}
34+
data, ok := value.(string)
35+
if !ok {
36+
return "", fmt.Errorf("returned value was not a string")
37+
}
38+
return data, nil
39+
}
40+
41+
func startMSSQLServer(c context.Context) (string, error) {
42+
{
43+
_, err := exec.Command("docker", "pull", "mcr.microsoft.com/mssql/server:2022-latest").CombinedOutput()
44+
if err != nil {
45+
return "", fmt.Errorf("docker pull: mssql/server:2022-latest %w", err)
46+
}
47+
}
48+
49+
var exists bool
50+
{
51+
cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_mssql")
52+
// This means we've already started the container
53+
exists = cmd.Run() == nil
54+
}
55+
56+
if !exists {
57+
cmd := exec.Command("docker", "run",
58+
"--name", "sqlc_sqltest_docker_mssql",
59+
"-e", "ACCEPT_EULA=Y",
60+
"-e", "MSSQL_SA_PASSWORD=MySecretPassword1!",
61+
"-e", "MSSQL_PID=Developer",
62+
"-p", "1433:1433",
63+
"-d",
64+
"mcr.microsoft.com/mssql/server:2022-latest",
65+
)
66+
67+
output, err := cmd.CombinedOutput()
68+
fmt.Println(string(output))
69+
70+
msg := `Conflict. The container name "/sqlc_sqltest_docker_mssql" is already in use by container`
71+
if !strings.Contains(string(output), msg) && err != nil {
72+
return "", err
73+
}
74+
}
75+
76+
// MSSQL takes longer to start than MySQL/PostgreSQL
77+
ctx, cancel := context.WithTimeout(c, 60*time.Second)
78+
defer cancel()
79+
80+
// Create a ticker that fires every 500ms (MSSQL takes longer to start)
81+
ticker := time.NewTicker(500 * time.Millisecond)
82+
defer ticker.Stop()
83+
84+
uri := "sqlserver://sa:MySecretPassword1!@localhost:1433?database=master"
85+
86+
db, err := sql.Open("sqlserver", uri)
87+
if err != nil {
88+
return "", fmt.Errorf("sql.Open: %w", err)
89+
}
90+
91+
defer db.Close()
92+
93+
for {
94+
select {
95+
case <-ctx.Done():
96+
return "", fmt.Errorf("timeout reached: %w", ctx.Err())
97+
98+
case <-ticker.C:
99+
// Run your function here
100+
if err := db.PingContext(ctx); err != nil {
101+
continue
102+
}
103+
return uri, nil
104+
}
105+
}
106+
}

internal/sqltest/native/mssql.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package native
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"log/slog"
8+
"os/exec"
9+
"time"
10+
11+
_ "github.com/microsoft/go-mssqldb"
12+
"golang.org/x/sync/singleflight"
13+
)
14+
15+
var mssqlFlight singleflight.Group
16+
var mssqlURI string
17+
18+
// StartMSSQLServer starts an existing MSSQL Server installation natively (without Docker).
19+
func StartMSSQLServer(ctx context.Context) (string, error) {
20+
if err := Supported(); err != nil {
21+
return "", err
22+
}
23+
if mssqlURI != "" {
24+
return mssqlURI, nil
25+
}
26+
value, err, _ := mssqlFlight.Do("mssql", func() (interface{}, error) {
27+
uri, err := startMSSQLServer(ctx)
28+
if err != nil {
29+
return "", err
30+
}
31+
mssqlURI = uri
32+
return uri, nil
33+
})
34+
if err != nil {
35+
return "", err
36+
}
37+
data, ok := value.(string)
38+
if !ok {
39+
return "", fmt.Errorf("returned value was not a string")
40+
}
41+
return data, nil
42+
}
43+
44+
func startMSSQLServer(ctx context.Context) (string, error) {
45+
// Standard URI for test MSSQL - matches docker-compose.yml password
46+
uri := "sqlserver://sa:MySecretPassword1!@localhost:1433?database=master"
47+
48+
// Try to connect first - it might already be running
49+
if err := waitForMSSQL(ctx, uri, 500*time.Millisecond); err == nil {
50+
slog.Info("native/mssql", "status", "already running")
51+
return uri, nil
52+
}
53+
54+
// Check if MSSQL is installed
55+
if _, err := exec.LookPath("sqlservr"); err != nil {
56+
// Also check for the mssql-conf tool
57+
if _, err := exec.LookPath("/opt/mssql/bin/mssql-conf"); err != nil {
58+
return "", fmt.Errorf("MSSQL Server is not installed")
59+
}
60+
}
61+
62+
// Try to start existing MSSQL service
63+
slog.Info("native/mssql", "status", "starting existing service")
64+
if err := startMSSQLService(); err != nil {
65+
slog.Debug("native/mssql", "start-error", err)
66+
} else {
67+
// Wait for MSSQL to be ready
68+
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
69+
defer cancel()
70+
71+
if err := waitForMSSQL(waitCtx, uri, 30*time.Second); err == nil {
72+
return uri, nil
73+
}
74+
}
75+
76+
return "", fmt.Errorf("MSSQL Server is not installed or could not be started")
77+
}
78+
79+
func startMSSQLService() error {
80+
// Try systemctl first
81+
cmd := exec.Command("sudo", "systemctl", "start", "mssql-server")
82+
if err := cmd.Run(); err == nil {
83+
// Give MSSQL time to fully initialize
84+
time.Sleep(3 * time.Second)
85+
return nil
86+
}
87+
88+
// Try service command
89+
cmd = exec.Command("sudo", "service", "mssql-server", "start")
90+
if err := cmd.Run(); err == nil {
91+
time.Sleep(3 * time.Second)
92+
return nil
93+
}
94+
95+
return fmt.Errorf("could not start MSSQL service")
96+
}
97+
98+
func waitForMSSQL(ctx context.Context, uri string, timeout time.Duration) error {
99+
deadline := time.Now().Add(timeout)
100+
ticker := time.NewTicker(500 * time.Millisecond)
101+
defer ticker.Stop()
102+
103+
var lastErr error
104+
for {
105+
select {
106+
case <-ctx.Done():
107+
return fmt.Errorf("context cancelled: %w (last error: %v)", ctx.Err(), lastErr)
108+
case <-ticker.C:
109+
if time.Now().After(deadline) {
110+
return fmt.Errorf("timeout waiting for MSSQL (last error: %v)", lastErr)
111+
}
112+
db, err := sql.Open("sqlserver", uri)
113+
if err != nil {
114+
lastErr = err
115+
slog.Debug("native/mssql", "open-attempt", err)
116+
continue
117+
}
118+
// Use a short timeout for ping to avoid hanging
119+
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
120+
err = db.PingContext(pingCtx)
121+
cancel()
122+
if err != nil {
123+
lastErr = err
124+
db.Close()
125+
continue
126+
}
127+
db.Close()
128+
return nil
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)