Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/modules/mssql.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom

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

#### Init Scripts

- 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>

If you need to execute SQL files when the container starts, you can use `mssql.WithInitSQL(files
...io.Reader)` with one or more `*.sql` files. The files will be executed in order after the
container is ready.

<!--codeinclude-->
[Example of SQL script](../../modules/mssql/testdata/seed.sql)
<!--/codeinclude-->

This will:

1. Copy each file into the container.
2. Execute them using `sqlcmd` after the container is ready.

#### Image

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.
Expand Down
54 changes: 54 additions & 0 deletions modules/mssql/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package mssql
import (
"context"
"fmt"
"io"
"strings"

"github.com/testcontainers/testcontainers-go"
tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -41,6 +43,58 @@ func WithPassword(password string) testcontainers.CustomizeRequestOption {
}
}

// WithInitSQL adds SQL scripts to be executed after the container is ready.
// The scripts are executed in the order they are provided using sqlcmd tool.
func WithInitSQL(files ...io.Reader) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
hooks := make([]testcontainers.ContainerHook, 0, len(files))

for i, script := range files {
content, err := io.ReadAll(script)
if err != nil {
return fmt.Errorf("failed to read script: %w", err)
}

hook := func(ctx context.Context, c testcontainers.Container) error {
password := defaultPassword
if req.Env["MSSQL_SA_PASSWORD"] != "" {
password = req.Env["MSSQL_SA_PASSWORD"]
}

// targetPath is a dummy path to store the script in the container
targetPath := "/tmp/" + fmt.Sprintf("script_%d.sql", i)
if err := c.CopyToContainer(ctx, content, targetPath, 0o644); err != nil {
return fmt.Errorf("failed to copy script to container: %w", err)
}

// NOTE: we add both legacy and new mssql-tools paths to ensure compatibility
envOpts := tcexec.WithEnv([]string{
"PATH=/opt/mssql-tools18/bin:/opt/mssql-tools/bin:$PATH",
})
cmd := []string{
"sqlcmd",
"-S", "localhost",
"-U", defaultUsername,
"-P", password,
"-No",
"-i", targetPath,
}
if _, _, err := c.Exec(ctx, cmd, envOpts); err != nil {
return fmt.Errorf("failed to execute SQL script %q using sqlcmd: %w", targetPath, err)
}
return nil
}
hooks = append(hooks, hook)
}

req.LifecycleHooks = append(req.LifecycleHooks, testcontainers.ContainerLifecycleHooks{
PostReadies: hooks,
})

return nil
}
}

// Deprecated: use Run instead
// RunContainer creates an instance of the MSSQLServer container type
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*MSSQLServerContainer, error) {
Expand Down
83 changes: 83 additions & 0 deletions modules/mssql/mssql_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package mssql_test

import (
"bytes"
"context"
"database/sql"
_ "embed"
"testing"

_ "github.com/microsoft/go-mssqldb"
Expand Down Expand Up @@ -128,3 +130,84 @@ func TestMSSQLServerWithInvalidPassword(t *testing.T) {
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
}

//go:embed testdata/seed.sql
var seedSQLContent []byte

// tests that a container can be created with a DDL script
func TestMSSQLServerWithScriptsDDL(t *testing.T) {
const password = "MyCustom@Passw0rd"

// assertContainer contains the logic for asserting the test
assertContainer := func(t *testing.T, ctx context.Context, image string, options ...testcontainers.ContainerCustomizer) {
t.Helper()

ctr, err := mssql.Run(ctx,
image,
append([]testcontainers.ContainerCustomizer{mssql.WithAcceptEULA()}, options...)...,
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)

connectionString, err := ctr.ConnectionString(ctx)
require.NoError(t, err)

db, err := sql.Open("sqlserver", connectionString)
require.NoError(t, err)
defer db.Close()

err = db.PingContext(ctx)
require.NoError(t, err)

rows, err := db.QueryContext(ctx, "SELECT * FROM pizza_palace.pizzas")
require.NoError(t, err)
defer rows.Close()

type Pizza struct {
ID int
ToppingName string
Deliciousness string
}

want := []Pizza{
{1, "Pineapple", "Controversial but tasty"},
{2, "Pepperoni", "Classic never fails"},
}
got := make([]Pizza, 0, len(want))

for rows.Next() {
var p Pizza
err := rows.Scan(&p.ID, &p.ToppingName, &p.Deliciousness)
require.NoError(t, err)
got = append(got, p)
}

require.EqualValues(t, want, got)
}

ctx := context.Background()

t.Run("WithPassword/beforeWithScripts", func(t *testing.T) {
assertContainer(t, ctx,
"mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04",
mssql.WithPassword(password),
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
)
})

t.Run("WithPassword/afterWithScripts", func(t *testing.T) {
assertContainer(t, ctx,
"mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04",
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
mssql.WithPassword(password),
)
})

t.Run("2019-CU30-ubuntu-20.04/oldSQLCmd", func(t *testing.T) {
assertContainer(t, ctx,
"mcr.microsoft.com/mssql/server:2019-CU30-ubuntu-20.04",
mssql.WithPassword(password),
mssql.WithInitSQL(bytes.NewReader(seedSQLContent)),
)
})
}
14 changes: 14 additions & 0 deletions modules/mssql/testdata/seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE SCHEMA pizza_palace;
GO

CREATE TABLE pizza_palace.pizzas (
ID INT PRIMARY KEY IDENTITY,
ToppingName NVARCHAR(100),
Deliciousness NVARCHAR(100) UNIQUE
);
GO

INSERT INTO pizza_palace.pizzas (ToppingName, Deliciousness) VALUES
('Pineapple', 'Controversial but tasty'),
('Pepperoni', 'Classic never fails')
GO
Loading