Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ updates:
- /modules/elasticsearch
- /modules/etcd
- /modules/fakegcsserver
- /modules/fakesnow
- /modules/firebird
- /modules/forgejo
- /modules/gcloud
Expand Down
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
"name": "module / fakegcsserver",
"path": "../modules/fakegcsserver"
},
{
"name": "module / fakesnow",
"path": "../modules/fakesnow"
},
{
"name": "module / firebird",
"path": "../modules/firebird"
Expand Down
84 changes: 84 additions & 0 deletions docs/modules/fakesnow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# FakeSnow

Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

[fakesnow](https://github.com/tekumara/fakesnow) is a Snowflake fake backed by DuckDB. It runs as an
HTTP server speaking the Snowflake REST API, so the official
[gosnowflake](https://github.com/snowflakedb/gosnowflake) driver connects to it unmodified and
Snowflake-flavoured SQL such as `VARIANT`, `LATERAL FLATTEN` and `QUALIFY` works without a Snowflake account.

## Adding this module to your project dependencies

Please run the following command to add the FakeSnow module to your Go dependencies:

```
go get github.com/testcontainers/testcontainers-go/modules/fakesnow
```

## Usage example

<!--codeinclude-->
[Creating a FakeSnow container](../../modules/fakesnow/examples_test.go) inside_block:ExampleRun
<!--/codeinclude-->

Connecting to it with `database/sql`:

<!--codeinclude-->
[Querying a FakeSnow container](../../modules/fakesnow/examples_test.go) inside_block:ExampleContainer_ConnectionString
<!--/codeinclude-->

fakesnow starts with no databases, so create the one the DSN points at before using it.

## Module Reference

### Run function

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

The FakeSnow module exposes one entrypoint function to create the FakeSnow container, and this function receives three parameters:

```golang
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
```

- `context.Context`, the Go context.
- `string`, the Docker image to use.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

#### Image

Use the second argument in the `Run` function to set a valid Docker image.
In example: `Run(context.Background(), "ghcr.io/tekumara/fakesnow:0.11.21")`.

### Container Options

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

{% include "../features/common_functional_options_list.md" %}

#### Connection details

fakesnow accepts any credentials, so these options only change what the driver sends.

- `WithAccount(account string)`, the account, `fakesnow` by default. It cannot be empty: gosnowflake
then derives the account from the host, which has no account part when the host is a container address.
- `WithUsername(user string)`, the user, `fakesnow` by default.
- `WithPassword(password string)`, the password, `fakesnow` by default.
- `WithDatabase(database string)`, the database in the DSN, `db1` by default. It is not created for you.
- `WithSchema(schema string)`, the schema in the DSN, `schema1` by default. It is not created for you.

### Container Methods

The FakeSnow container exposes the following methods:

#### ConnectionString

- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

Returns a gosnowflake DSN for the container, for use with `sql.Open("snowflake", dsn)`.

<!--codeinclude-->
[Get connection string](../../modules/fakesnow/fakesnow.go) inside_block:ConnectionString
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ nav:
- modules/elasticsearch.md
- modules/etcd.md
- modules/fakegcsserver.md
- modules/fakesnow.md
- modules/firebird.md
- modules/forgejo.md
- modules/gcloud.md
Expand Down
5 changes: 5 additions & 0 deletions modules/fakesnow/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-fakesnow
89 changes: 89 additions & 0 deletions modules/fakesnow/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package fakesnow_test

import (
"context"
"database/sql"
"fmt"
"log"

_ "github.com/snowflakedb/gosnowflake"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/fakesnow"
)

func ExampleRun() {
ctx := context.Background()

fakesnowContainer, err := fakesnow.Run(ctx, "ghcr.io/tekumara/fakesnow:0.11.21")
defer func() {
if err := testcontainers.TerminateContainer(fakesnowContainer); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Printf("failed to start container: %s", err)
return
}
// }

state, err := fakesnowContainer.State(ctx)
if err != nil {
log.Printf("failed to get container state: %s", err)
return
}

fmt.Println(state.Running)

// Output:
// true
}

func ExampleContainer_ConnectionString() {
ctx := context.Background()

fakesnowContainer, err := fakesnow.Run(ctx, "ghcr.io/tekumara/fakesnow:0.11.21")
defer func() {
if err := testcontainers.TerminateContainer(fakesnowContainer); err != nil {
log.Printf("failed to terminate container: %s", err)
}
}()
if err != nil {
log.Printf("failed to start container: %s", err)
return
}

dsn, err := fakesnowContainer.ConnectionString(ctx)
if err != nil {
log.Printf("failed to get connection string: %s", err)
return
}

db, err := sql.Open("snowflake", dsn)
if err != nil {
log.Printf("failed to open database: %s", err)
return
}
defer db.Close()

// fakesnow starts empty, so create what the DSN points at.
if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS db1"); err != nil {
log.Printf("failed to create database: %s", err)
return
}
if _, err := db.Exec("CREATE SCHEMA IF NOT EXISTS db1.schema1"); err != nil {
log.Printf("failed to create schema: %s", err)
return
}

var planet string
if err := db.QueryRow(`SELECT PARSE_JSON('{"planet":"saturn"}'):planet::varchar`).Scan(&planet); err != nil {
log.Printf("failed to query: %s", err)
return
}

fmt.Println(planet)

// Output:
// saturn
}
94 changes: 94 additions & 0 deletions modules/fakesnow/fakesnow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Package fakesnow provides a Testcontainers module for fakesnow
// (https://github.com/tekumara/fakesnow), a Snowflake fake backed by DuckDB.
// It runs as an HTTP server speaking the Snowflake REST API, so the official
// gosnowflake driver connects to it unmodified.
package fakesnow

import (
"context"
"fmt"

sf "github.com/snowflakedb/gosnowflake"

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

// defaultPort is the port fakesnow serves the Snowflake REST API on.
const defaultPort = "64616/tcp"

// Container represents the fakesnow container type used in the module.
type Container struct {
testcontainers.Container
opts options
}

// ConnectionString returns a gosnowflake DSN for the container, for use with
// sql.Open("snowflake", dsn).
//
// The account is always part of the DSN. gosnowflake otherwise derives it from
// the host, which has no account part when the host is a container address.
func (c *Container) ConnectionString(ctx context.Context) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", fmt.Errorf("get host: %w", err)
}

port, err := c.MappedPort(ctx, defaultPort)
if err != nil {
return "", fmt.Errorf("get mapped port: %w", err)
}

dsn, err := sf.DSN(&sf.Config{
Account: c.opts.account,
User: c.opts.user,
Password: c.opts.password,
Host: host,
Port: int(port.Num()),
Protocol: "http",
Database: c.opts.database,
Schema: c.opts.schema,
})
if err != nil {
return "", fmt.Errorf("build dsn: %w", err)
}

return dsn, nil
}

// Run creates an instance of the fakesnow container type.
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
settings := defaultOptions()
for _, opt := range opts {
if o, ok := opt.(Option); ok {
if err := o(&settings); err != nil {
return nil, fmt.Errorf("fakesnow option: %w", err)
}
}
}

moduleOpts := make([]testcontainers.ContainerCustomizer, 0, 2+len(opts))
moduleOpts = append(moduleOpts,
testcontainers.WithExposedPorts(defaultPort),
// The log line is what fakesnow prints once it is serving. The port on
// its own is not enough: docker publishes it before the application
// binds, so a connection can still be refused after it looks ready.
testcontainers.WithWaitStrategy(wait.ForAll(
wait.ForLog("Application startup complete"),
wait.ForListeningPort(defaultPort),
)),
)
moduleOpts = append(moduleOpts, opts...)

ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
var c *Container
if ctr != nil {
c = &Container{Container: ctr, opts: settings}
}

if err != nil {
return c, fmt.Errorf("run fakesnow: %w", err)
}

return c, nil
}
Loading
Loading