Skip to content

Commit 2fffed1

Browse files
feat(mssql): add WithInitSQL function (#2988)
* feat(mssql): add WithInitSQL function * test(mssql): add test for WithInitSQL * docs(mssql): add docs for WithInitSQL --------- Co-authored-by: Manuel de la Peña <[email protected]>
1 parent 643a6f9 commit 2fffed1

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

docs/modules/mssql.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
4848

4949
When starting the MS SQL Server container, you can pass options in a variadic way to configure it.
5050

51+
#### Init Scripts
52+
53+
- Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
54+
55+
If you need to execute SQL files when the container starts, you can use `mssql.WithInitSQL(files
56+
...io.Reader)` with one or more `*.sql` files. The files will be executed in order after the
57+
container is ready.
58+
59+
<!--codeinclude-->
60+
[Example of SQL script](../../modules/mssql/testdata/seed.sql)
61+
<!--/codeinclude-->
62+
63+
This will:
64+
65+
1. Copy each file into the container.
66+
2. Execute them using `sqlcmd` after the container is ready.
67+
5168
#### Image
5269

5370
If you need to set a different MS SQL Server Docker image, you can set a valid Docker image as the second argument in the `Run` function.

modules/mssql/mssql.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package mssql
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"strings"
78

89
"github.com/testcontainers/testcontainers-go"
10+
tcexec "github.com/testcontainers/testcontainers-go/exec"
911
"github.com/testcontainers/testcontainers-go/wait"
1012
)
1113

@@ -41,6 +43,58 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption {
4143
}
4244
}
4345

46+
// WithInitSQL adds SQL scripts to be executed after the container is ready.
47+
// The scripts are executed in the order they are provided using sqlcmd tool.
48+
func WithInitSQL(files ...io.Reader) testcontainers.CustomizeRequestOption {
49+
return func(req *testcontainers.GenericContainerRequest) error {
50+
hooks := make([]testcontainers.ContainerHook, 0, len(files))
51+
52+
for i, script := range files {
53+
content, err := io.ReadAll(script)
54+
if err != nil {
55+
return fmt.Errorf("failed to read script: %w", err)
56+
}
57+
58+
hook := func(ctx context.Context, c testcontainers.Container) error {
59+
password := defaultPassword
60+
if req.Env["MSSQL_SA_PASSWORD"] != "" {
61+
password = req.Env["MSSQL_SA_PASSWORD"]
62+
}
63+
64+
// targetPath is a dummy path to store the script in the container
65+
targetPath := "/tmp/" + fmt.Sprintf("script_%d.sql", i)
66+
if err := c.CopyToContainer(ctx, content, targetPath, 0o644); err != nil {
67+
return fmt.Errorf("failed to copy script to container: %w", err)
68+
}
69+
70+
// NOTE: we add both legacy and new mssql-tools paths to ensure compatibility
71+
envOpts := tcexec.WithEnv([]string{
72+
"PATH=/opt/mssql-tools18/bin:/opt/mssql-tools/bin:$PATH",
73+
})
74+
cmd := []string{
75+
"sqlcmd",
76+
"-S", "localhost",
77+
"-U", defaultUsername,
78+
"-P", password,
79+
"-No",
80+
"-i", targetPath,
81+
}
82+
if _, _, err := c.Exec(ctx, cmd, envOpts); err != nil {
83+
return fmt.Errorf("failed to execute SQL script %q using sqlcmd: %w", targetPath, err)
84+
}
85+
return nil
86+
}
87+
hooks = append(hooks, hook)
88+
}
89+
90+
req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{
91+
PostReadies: hooks,
92+
})
93+
94+
return nil
95+
}
96+
}
97+
4498
// Deprecated: use Run instead
4599
// RunContainer creates an instance of the MSSQLServer container type
46100
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MSSQLServerContainer, error) {

modules/mssql/mssql_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package mssql_test
22

33
import (
4+
"bytes"
45
"context"
56
"database/sql"
7+
_ "embed"
68
"testing"
79

810
_ "github.com/microsoft/go-mssqldb"
@@ -128,3 +130,84 @@ func TestMSSQLServerWithInvalidPassword(t *testing.T) {
128130
testcontainers.CleanupContainer(t, ctr)
129131
require.NoError(t, err)
130132
}
133+
134+
//go:embed testdata/seed.sql
135+
var seedSQLContent []byte
136+
137+
// tests that a container can be created with a DDL script
138+
func TestMSSQLServerWithScriptsDDL(t *testing.T) {
139+
const password = "MyCustom@Passw0rd"
140+
141+
// assertContainer contains the logic for asserting the test
142+
assertContainer := func(t *testing.T, ctx context.Context, image string, options ...testcontainers.ContainerCustomizer) {
143+
t.Helper()
144+
145+
ctr, err := mssql.Run(ctx,
146+
image,
147+
append([]testcontainers.ContainerCustomizer{mssql.WithAcceptEULA()}, options...)...,
148+
)
149+
testcontainers.CleanupContainer(t, ctr)
150+
require.NoError(t, err)
151+
152+
connectionString, err := ctr.ConnectionString(ctx)
153+
require.NoError(t, err)
154+
155+
db, err := sql.Open("sqlserver", connectionString)
156+
require.NoError(t, err)
157+
defer db.Close()
158+
159+
err = db.PingContext(ctx)
160+
require.NoError(t, err)
161+
162+
rows, err := db.QueryContext(ctx, "SELECT * FROM pizza_palace.pizzas")
163+
require.NoError(t, err)
164+
defer rows.Close()
165+
166+
type Pizza struct {
167+
ID int
168+
ToppingName string
169+
Deliciousness string
170+
}
171+
172+
want := []Pizza{
173+
{1, "Pineapple", "Controversial but tasty"},
174+
{2, "Pepperoni", "Classic never fails"},
175+
}
176+
got := make([]Pizza, 0, len(want))
177+
178+
for rows.Next() {
179+
var p Pizza
180+
err := rows.Scan(&p.ID, &p.ToppingName, &p.Deliciousness)
181+
require.NoError(t, err)
182+
got = append(got, p)
183+
}
184+
185+
require.EqualValues(t, want, got)
186+
}
187+
188+
ctx := context.Background()
189+
190+
t.Run("WithPassword/beforeWithScripts", func(t *testing.T) {
191+
assertContainer(t, ctx,
192+
"mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04",
193+
mssql.WithPassword(password),
194+
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
195+
)
196+
})
197+
198+
t.Run("WithPassword/afterWithScripts", func(t *testing.T) {
199+
assertContainer(t, ctx,
200+
"mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04",
201+
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
202+
mssql.WithPassword(password),
203+
)
204+
})
205+
206+
t.Run("2019-CU30-ubuntu-20.04/oldSQLCmd", func(t *testing.T) {
207+
assertContainer(t, ctx,
208+
"mcr.microsoft.com/mssql/server:2019-CU30-ubuntu-20.04",
209+
mssql.WithPassword(password),
210+
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
211+
)
212+
})
213+
}

modules/mssql/testdata/seed.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE SCHEMA pizza_palace;
2+
GO
3+
4+
CREATE TABLE pizza_palace.pizzas (
5+
ID INT PRIMARY KEY IDENTITY,
6+
ToppingName NVARCHAR(100),
7+
Deliciousness NVARCHAR(100) UNIQUE
8+
);
9+
GO
10+
11+
INSERT INTO pizza_palace.pizzas (ToppingName, Deliciousness) VALUES
12+
('Pineapple', 'Controversial but tasty'),
13+
('Pepperoni', 'Classic never fails')
14+
GO

0 commit comments

Comments
 (0)