Skip to content

Commit cec1ed9

Browse files
committed
Add ClickHouse test database adapters
Docker and local connection adapters for integration testing. Enables end-to-end testing with real ClickHouse instances.
1 parent 14e9917 commit cec1ed9

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package docker
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"log/slog"
8+
"os/exec"
9+
"strings"
10+
"time"
11+
12+
_ "github.com/ClickHouse/clickhouse-go/v2"
13+
)
14+
15+
var clickhouseHost string
16+
17+
func StartClickHouseServer(c context.Context) (string, error) {
18+
if err := Installed(); err != nil {
19+
return "", err
20+
}
21+
if clickhouseHost != "" {
22+
return clickhouseHost, nil
23+
}
24+
value, err, _ := flight.Do("clickhouse", func() (interface{}, error) {
25+
host, err := startClickHouseServer(c)
26+
if err != nil {
27+
return "", err
28+
}
29+
clickhouseHost = host
30+
return host, err
31+
})
32+
if err != nil {
33+
return "", err
34+
}
35+
data, ok := value.(string)
36+
if !ok {
37+
return "", fmt.Errorf("returned value was not a string")
38+
}
39+
return data, nil
40+
}
41+
42+
func startClickHouseServer(c context.Context) (string, error) {
43+
{
44+
_, err := exec.Command("docker", "pull", "clickhouse:lts").CombinedOutput()
45+
if err != nil {
46+
return "", fmt.Errorf("docker pull: clickhouse:lts %w", err)
47+
}
48+
}
49+
50+
var exists bool
51+
{
52+
cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_clickhouse")
53+
// This means we've already started the container
54+
exists = cmd.Run() == nil
55+
}
56+
57+
if !exists {
58+
cmd := exec.Command("docker", "run",
59+
"--name", "sqlc_sqltest_docker_clickhouse",
60+
"-p", "9000:9000",
61+
"-p", "8123:8123",
62+
"-e", "CLICKHOUSE_DB=default",
63+
"-e", "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1",
64+
"-d",
65+
"clickhouse:lts",
66+
)
67+
68+
output, err := cmd.CombinedOutput()
69+
fmt.Println(string(output))
70+
71+
msg := `Conflict. The container name "/sqlc_sqltest_docker_clickhouse" is already in use by container`
72+
if !strings.Contains(string(output), msg) && err != nil {
73+
return "", err
74+
}
75+
}
76+
77+
ctx, cancel := context.WithTimeout(c, 10*time.Second)
78+
defer cancel()
79+
80+
// Create a ticker that fires every 10ms
81+
ticker := time.NewTicker(10 * time.Millisecond)
82+
defer ticker.Stop()
83+
84+
// ClickHouse DSN format: clickhouse://host:port
85+
dsn := "clickhouse://localhost:9000/default"
86+
87+
for {
88+
select {
89+
case <-ctx.Done():
90+
return "", fmt.Errorf("timeout reached: %w", ctx.Err())
91+
92+
case <-ticker.C:
93+
db, err := sql.Open("clickhouse", dsn)
94+
if err != nil {
95+
slog.Debug("sqltest", "open", err)
96+
continue
97+
}
98+
99+
if err := db.PingContext(ctx); err != nil {
100+
slog.Debug("sqltest", "ping", err)
101+
db.Close()
102+
continue
103+
}
104+
105+
db.Close()
106+
return dsn, nil
107+
}
108+
}
109+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package local
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"hash/fnv"
8+
"os"
9+
"strings"
10+
"testing"
11+
12+
migrate "github.com/sqlc-dev/sqlc/internal/migrations"
13+
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
14+
"github.com/sqlc-dev/sqlc/internal/sqltest/docker"
15+
16+
_ "github.com/ClickHouse/clickhouse-go/v2"
17+
)
18+
19+
func ClickHouse(t *testing.T, migrations []string) string {
20+
ctx := context.Background()
21+
t.Helper()
22+
23+
dburi := os.Getenv("CLICKHOUSE_SERVER_URI")
24+
if dburi == "" {
25+
if ierr := docker.Installed(); ierr == nil {
26+
u, err := docker.StartClickHouseServer(ctx)
27+
if err != nil {
28+
t.Fatal(err)
29+
}
30+
dburi = u
31+
} else {
32+
t.Skip("CLICKHOUSE_SERVER_URI is empty")
33+
}
34+
}
35+
36+
// Open connection to ClickHouse
37+
db, err := sql.Open("clickhouse", dburi)
38+
if err != nil {
39+
t.Fatalf("ClickHouse connection failed: %s", err)
40+
}
41+
defer db.Close()
42+
43+
var seed []string
44+
files, err := sqlpath.Glob(migrations)
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
49+
h := fnv.New64()
50+
for _, f := range files {
51+
blob, err := os.ReadFile(f)
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
h.Write(blob)
56+
seed = append(seed, migrate.RemoveRollbackStatements(string(blob)))
57+
}
58+
59+
// Create unique database name
60+
name := fmt.Sprintf("sqlc_test_%x", h.Sum(nil))
61+
62+
// Drop database if it exists (ClickHouse style)
63+
dropQuery := fmt.Sprintf(`DROP DATABASE IF EXISTS %s`, name)
64+
if _, err := db.ExecContext(ctx, dropQuery); err != nil {
65+
t.Logf("could not drop database (may not exist): %s", err)
66+
}
67+
68+
// Create new database
69+
createQuery := fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS %s`, name)
70+
if _, err := db.ExecContext(ctx, createQuery); err != nil {
71+
t.Fatalf("failed to create database: %s", err)
72+
}
73+
74+
// Execute migration scripts
75+
dbWithDatabase := fmt.Sprintf("%s?database=%s", dburi, name)
76+
dbConn, err := sql.Open("clickhouse", dbWithDatabase)
77+
if err != nil {
78+
t.Fatalf("ClickHouse connection to new database failed: %s", err)
79+
}
80+
defer dbConn.Close()
81+
82+
for _, q := range seed {
83+
if len(strings.TrimSpace(q)) == 0 {
84+
continue
85+
}
86+
if _, err := dbConn.ExecContext(ctx, q); err != nil {
87+
t.Fatalf("migration failed: %s: %s", q, err)
88+
}
89+
}
90+
91+
// Register cleanup
92+
t.Cleanup(func() {
93+
if _, err := db.ExecContext(ctx, dropQuery); err != nil {
94+
t.Logf("failed cleaning up database: %s", err)
95+
}
96+
})
97+
98+
return dbWithDatabase
99+
}

0 commit comments

Comments
 (0)