Skip to content

Commit 6fc4352

Browse files
committed
codegen/json: leave out an unset type_expr; test-setup: install ClickHouse and SQL Server
A column's type_expr is only set when the analysis core typed it, and the JSON plugin wrote every other column with an explicit null, which put a line on every column of every request the legacy path produced. Drop the member when it is unset instead, so those requests read as they did before the member existed; the goldens on the legacy path are back to what they were. sqlc-test-setup now installs and starts ClickHouse and SQL Server as well, so the examples need no service containers from CI's host. ClickHouse comes from the release tarball goldeneye pins, checked against its SHA-512 and cached where goldeneye caches it, and runs from a configuration the tool writes that gives the default user the password the tests use. SQL Server comes from Microsoft's apt repository for Ubuntu 22.04 and 24.04, set up non-interactively with the EULA accepted, and starts through systemd where there is one and in the background otherwise. The examples job in CI installs and starts every database through the tool. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
1 parent 29c7517 commit 6fc4352

8 files changed

Lines changed: 5531 additions & 9942 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,28 +73,10 @@ jobs:
7373

7474
# The examples run generated code against every database sqlc supports.
7575
# They are a module of their own, so their drivers are not dependencies
76-
# of sqlc, and cgo stays on for the DuckDB driver. ClickHouse and SQL
77-
# Server run as services; sqlc-test-setup installs PostgreSQL, MySQL and
78-
# Spanner Omni on the runner.
76+
# of sqlc, and cgo stays on for the DuckDB driver. sqlc-test-setup
77+
# installs and starts every database on the runner.
7978
examples:
8079
runs-on: ubuntu-24.04
81-
services:
82-
clickhouse:
83-
image: clickhouse/clickhouse-server:25.8
84-
ports:
85-
- 9000:9000
86-
- 8123:8123
87-
env:
88-
# A password opens the default user to connections from beyond
89-
# the container; without one the image restricts it to localhost.
90-
CLICKHOUSE_PASSWORD: "mysecretpassword"
91-
mssql:
92-
image: mcr.microsoft.com/mssql/server:2025-latest
93-
ports:
94-
- 1433:1433
95-
env:
96-
ACCEPT_EULA: "Y"
97-
MSSQL_SA_PASSWORD: "Mysecretpassword1!"
9880
steps:
9981
- uses: actions/checkout@v7
10082
- uses: actions/setup-go@v7

‎CLAUDE.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This document provides essential information for working with the sqlc codebase,
1212

1313
## Database Setup with sqlc-test-setup
1414

15-
The `sqlc-test-setup` tool (`cmd/sqlc-test-setup/`) automates installing and starting PostgreSQL, MySQL and Spanner Omni for tests. Both commands are idempotent and safe to re-run, and take the databases to act on as arguments (`postgresql`, `mysql`, `spanner`); with none named they act on all three.
15+
The `sqlc-test-setup` tool (`cmd/sqlc-test-setup/`) automates installing and starting PostgreSQL, MySQL, ClickHouse, SQL Server and Spanner Omni for tests. Both commands are idempotent and safe to re-run, and take the databases to act on as arguments (`postgresql`, `mysql`, `clickhouse`, `mssql`, `spanner`); with none named they act on all five.
1616

1717
### Install databases
1818

@@ -24,6 +24,8 @@ This will:
2424
- Configure the apt proxy (if `http_proxy` is set, e.g. in Claude Code remote environments)
2525
- Install PostgreSQL via apt
2626
- Download and install MySQL 26.7 from Oracle's deb bundle
27+
- Download the ClickHouse 25.8 release into the user cache, where goldeneye caches it too
28+
- Install SQL Server 2025 from Microsoft's apt repository and run its setup, accepting the EULA on your behalf (Ubuntu 22.04 and 24.04 only)
2729
- Download the Spanner Omni standalone server release into the user cache (linux/amd64 only)
2830
- Resolve all dependencies automatically
2931
- Skip anything already installed
@@ -37,16 +39,20 @@ go run ./cmd/sqlc-test-setup start
3739
This will:
3840
- Start PostgreSQL and configure password auth (`postgres`/`postgres`)
3941
- Start MySQL via `mysqld_safe` and set root password (`mysecretpassword`)
42+
- Start a ClickHouse server in the background on localhost, with the default user's password `mysecretpassword`
43+
- Start SQL Server through systemd, or in the background where there is no systemd, with the sa password `Mysecretpassword1!`
4044
- Start a Spanner Omni single server in the background, serving plaintext gRPC on port 15000
4145
- Verify the connections
4246
- Skip steps that are already done (running services, existing config)
4347

4448
Connection URIs after start:
4549
- PostgreSQL: `postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable`
4650
- MySQL: `root:mysecretpassword@tcp(127.0.0.1:3306)/mysql`
51+
- ClickHouse: `clickhouse://default:mysecretpassword@127.0.0.1:9000` (`CLICKHOUSE_SERVER_URI`)
52+
- SQL Server: `sqlserver://sa:Mysecretpassword1!@127.0.0.1:1433?encrypt=disable` (`MSSQL_SERVER_URI`)
4753
- Spanner Omni: `localhost:15000` (`SPANNER_SERVER_URI`)
4854

49-
ClickHouse and SQL Server are not installed by the tool; `docker-compose.yml` runs them locally and CI runs them as services. Their tests read `CLICKHOUSE_SERVER_URI` and `MSSQL_SERVER_URI` and skip when unset.
55+
The examples' tests for ClickHouse, SQL Server and Spanner read those variables and skip when unset. `docker-compose.yml` runs the same databases for anyone who prefers containers.
5056

5157
### Run tests
5258

@@ -217,7 +223,7 @@ make start # Start database containers
217223
- **File:** `.github/workflows/ci.yml`
218224
- **Go Version:** 1.26.4
219225
- **Jobs:** `build` cross-compiles sqlc with cgo off; `test` runs sqlc's own tests; `examples` runs the examples module against every database; `vuln_check` runs `govulncheck`
220-
- **Database Setup:** `test` uses `sqlc-test-setup` (not Docker) to install and start PostgreSQL and MySQL directly on the runner; `examples` also installs Spanner Omni that way and runs ClickHouse and SQL Server as job services
226+
- **Database Setup:** `sqlc-test-setup` (not Docker) installs and starts the databases directly on the runner: PostgreSQL and MySQL for `test`, and all five for `examples`
221227
- **Test Command:** `gotestsum --junitfile junit.xml -- --tags=examples -timeout 20m ./...` in `test`, and `go test --tags=examples ./...` under `examples/` in `examples`
222228
- **Additional Checks:** `govulncheck` for vulnerability scanning
223229

‎cmd/sqlc-test-setup/clickhouse.go‎

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package main
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"crypto/sha512"
7+
"encoding/hex"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"log"
12+
"net"
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"runtime"
17+
"strings"
18+
"time"
19+
)
20+
21+
const (
22+
// clickhouseVersion is the ClickHouse release to install. It is the
23+
// release internal/goldeneye generates the dialect from, and the binary
24+
// is cached where goldeneye caches it, so the two share one download.
25+
clickhouseVersion = "25.8.2.29"
26+
27+
// clickhousePassword is the password the default user is given: the
28+
// one docker-compose.yml and CI use.
29+
clickhousePassword = "mysecretpassword"
30+
31+
clickhouseTCPPort = "9000"
32+
clickhouseHTTPPort = "8123"
33+
)
34+
35+
// clickhouseAsset is one downloadable build of ClickHouse: a
36+
// clickhouse-common-static tarball holding the binary at usr/bin/clickhouse,
37+
// with the SHA-512 ClickHouse publishes next to it.
38+
type clickhouseAsset struct {
39+
URL string
40+
SHA512 string
41+
}
42+
43+
var clickhouseAssets = map[string]clickhouseAsset{
44+
"linux/amd64": {
45+
URL: "https://github.com/ClickHouse/ClickHouse/releases/download/v" + clickhouseVersion + "-lts/clickhouse-common-static-" + clickhouseVersion + "-amd64.tgz",
46+
SHA512: "6ff0aa1ffac6e564970174422ecde0d645cdb96812247a6e544d39cad6d78a514265f90a2bc7b4bad49903cea96eddd16a415a45b2aeaf9164461be76331bdee",
47+
},
48+
"linux/arm64": {
49+
URL: "https://github.com/ClickHouse/ClickHouse/releases/download/v" + clickhouseVersion + "-lts/clickhouse-common-static-" + clickhouseVersion + "-arm64.tgz",
50+
SHA512: "68204ca4d4e472790f808ee376251fae82e58066a31f35a40d15d442ce5988d697f18a1208d28b8bb8e2dfad4b20b7fcb5107e2178472abcd97251b8de7f058e",
51+
},
52+
}
53+
54+
// clickhouseDir is where the release is cached, and the server's
55+
// configuration and data are kept under it.
56+
func clickhouseDir() (string, error) {
57+
cache, err := os.UserCacheDir()
58+
if err != nil {
59+
return "", err
60+
}
61+
return filepath.Join(cache, "sqlc-clickhouse", clickhouseVersion), nil
62+
}
63+
64+
func clickhouseBinary(dir string) string {
65+
return filepath.Join(dir, "clickhouse")
66+
}
67+
68+
// installClickHouse downloads the ClickHouse release for this platform and
69+
// unpacks its binary into the cache, checking the download against the
70+
// pinned SHA-512. It is a no-op when the binary is already there, and
71+
// skips platforms the release is not published for.
72+
func installClickHouse() error {
73+
log.Printf("--- Installing ClickHouse %s ---", clickhouseVersion)
74+
75+
platform := runtime.GOOS + "/" + runtime.GOARCH
76+
asset, ok := clickhouseAssets[platform]
77+
if !ok {
78+
log.Printf("clickhouse is not published for %s, skipping", platform)
79+
return nil
80+
}
81+
82+
dir, err := clickhouseDir()
83+
if err != nil {
84+
return err
85+
}
86+
binary := clickhouseBinary(dir)
87+
if _, err := os.Stat(binary); err == nil {
88+
log.Printf("clickhouse %s is already installed at %s", clickhouseVersion, binary)
89+
return nil
90+
}
91+
if err := os.MkdirAll(dir, 0o755); err != nil {
92+
return err
93+
}
94+
95+
archive := filepath.Join(dir, "clickhouse.tgz")
96+
log.Printf("downloading %s", asset.URL)
97+
if err := downloadFile(archive, asset.URL); err != nil {
98+
return fmt.Errorf("downloading clickhouse: %w", err)
99+
}
100+
defer os.Remove(archive)
101+
102+
sum, err := sha512File(archive)
103+
if err != nil {
104+
return err
105+
}
106+
if sum != asset.SHA512 {
107+
return fmt.Errorf("clickhouse download has SHA-512 %s, want %s", sum, asset.SHA512)
108+
}
109+
110+
log.Printf("unpacking the binary into %s", binary)
111+
if err := extractClickHouse(archive, binary); err != nil {
112+
return fmt.Errorf("unpacking clickhouse: %w", err)
113+
}
114+
return nil
115+
}
116+
117+
// extractClickHouse copies usr/bin/clickhouse out of the release tarball.
118+
func extractClickHouse(archive, binary string) error {
119+
f, err := os.Open(archive)
120+
if err != nil {
121+
return err
122+
}
123+
defer f.Close()
124+
gz, err := gzip.NewReader(f)
125+
if err != nil {
126+
return err
127+
}
128+
tr := tar.NewReader(gz)
129+
for {
130+
hdr, err := tr.Next()
131+
if errors.Is(err, io.EOF) {
132+
return errors.New("tarball does not contain usr/bin/clickhouse")
133+
}
134+
if err != nil {
135+
return err
136+
}
137+
if hdr.Typeflag != tar.TypeReg || !strings.HasSuffix(hdr.Name, "/usr/bin/clickhouse") {
138+
continue
139+
}
140+
tmp, err := os.CreateTemp(filepath.Dir(binary), "clickhouse-*.partial")
141+
if err != nil {
142+
return err
143+
}
144+
if _, err := io.Copy(tmp, tr); err != nil {
145+
tmp.Close()
146+
os.Remove(tmp.Name())
147+
return err
148+
}
149+
if err := tmp.Close(); err != nil {
150+
os.Remove(tmp.Name())
151+
return err
152+
}
153+
if err := os.Chmod(tmp.Name(), 0o755); err != nil {
154+
os.Remove(tmp.Name())
155+
return err
156+
}
157+
return os.Rename(tmp.Name(), binary)
158+
}
159+
}
160+
161+
// sha512File computes the SHA-512 hash of a file and returns the hex string.
162+
func sha512File(path string) (string, error) {
163+
f, err := os.Open(path)
164+
if err != nil {
165+
return "", err
166+
}
167+
defer f.Close()
168+
h := sha512.New()
169+
if _, err := io.Copy(h, f); err != nil {
170+
return "", err
171+
}
172+
return hex.EncodeToString(h.Sum(nil)), nil
173+
}
174+
175+
// clickhouseConfig is the server's configuration: it listens on localhost
176+
// only, keeps its data next to the configuration, and takes its users from
177+
// users.xml beside it.
178+
const clickhouseConfig = `<clickhouse>
179+
<logger>
180+
<level>warning</level>
181+
<console>1</console>
182+
</logger>
183+
<path>./data/</path>
184+
<tcp_port>` + clickhouseTCPPort + `</tcp_port>
185+
<http_port>` + clickhouseHTTPPort + `</http_port>
186+
<listen_host>127.0.0.1</listen_host>
187+
<users_config>users.xml</users_config>
188+
<user_directories>
189+
<users_xml>
190+
<path>users.xml</path>
191+
</users_xml>
192+
</user_directories>
193+
</clickhouse>
194+
`
195+
196+
// clickhouseUsers gives the default user a password. Without one the
197+
// server restricts the user to localhost, which is fine here, but the
198+
// tests, docker-compose.yml and CI all connect with the same password.
199+
const clickhouseUsers = `<clickhouse>
200+
<profiles>
201+
<default/>
202+
</profiles>
203+
<quotas>
204+
<default/>
205+
</quotas>
206+
<users>
207+
<default>
208+
<password>` + clickhousePassword + `</password>
209+
<networks>
210+
<ip>::/0</ip>
211+
</networks>
212+
<profile>default</profile>
213+
<quota>default</quota>
214+
</default>
215+
</users>
216+
</clickhouse>
217+
`
218+
219+
// startClickHouse starts a ClickHouse server in the background, serving
220+
// on localhost, and waits until it answers a query. It is a no-op when a
221+
// server already does, and skips platforms the release is not installed
222+
// on.
223+
func startClickHouse() error {
224+
log.Println("--- Starting ClickHouse ---")
225+
226+
dir, err := clickhouseDir()
227+
if err != nil {
228+
return err
229+
}
230+
binary := clickhouseBinary(dir)
231+
if _, err := os.Stat(binary); err != nil {
232+
if _, ok := clickhouseAssets[runtime.GOOS+"/"+runtime.GOARCH]; !ok {
233+
log.Printf("clickhouse is not published for %s/%s, skipping", runtime.GOOS, runtime.GOARCH)
234+
return nil
235+
}
236+
return fmt.Errorf("clickhouse is not installed: run `sqlc-test-setup install clickhouse` first")
237+
}
238+
239+
if clickhouseReady(binary) {
240+
log.Println("clickhouse is already running and accepting connections")
241+
return nil
242+
}
243+
244+
server := filepath.Join(dir, "server")
245+
if err := os.MkdirAll(filepath.Join(server, "data"), 0o755); err != nil {
246+
return err
247+
}
248+
if err := os.WriteFile(filepath.Join(server, "config.xml"), []byte(clickhouseConfig), 0o644); err != nil {
249+
return err
250+
}
251+
if err := os.WriteFile(filepath.Join(server, "users.xml"), []byte(clickhouseUsers), 0o644); err != nil {
252+
return err
253+
}
254+
logFile, err := os.Create(filepath.Join(server, "server.log"))
255+
if err != nil {
256+
return err
257+
}
258+
defer logFile.Close()
259+
260+
cmd := exec.Command(binary, "server", "--config-file="+filepath.Join(server, "config.xml"))
261+
cmd.Dir = server
262+
cmd.Stdout = logFile
263+
cmd.Stderr = logFile
264+
cmd.SysProcAttr = detachedProcess()
265+
log.Printf("starting %s server in %s", binary, server)
266+
if err := cmd.Start(); err != nil {
267+
return fmt.Errorf("starting clickhouse: %w", err)
268+
}
269+
if err := cmd.Process.Release(); err != nil {
270+
return err
271+
}
272+
273+
log.Println("waiting for clickhouse to accept connections")
274+
deadline := time.Now().Add(2 * time.Minute)
275+
for time.Now().Before(deadline) {
276+
if clickhouseReady(binary) {
277+
log.Println("clickhouse is accepting connections")
278+
return nil
279+
}
280+
time.Sleep(time.Second)
281+
}
282+
return fmt.Errorf("clickhouse did not start in time (see %s)", logFile.Name())
283+
}
284+
285+
// clickhouseReady reports whether a server answers on the TCP port with
286+
// the expected password.
287+
func clickhouseReady(binary string) bool {
288+
conn, err := net.DialTimeout("tcp", "127.0.0.1:"+clickhouseTCPPort, time.Second)
289+
if err != nil {
290+
return false
291+
}
292+
conn.Close()
293+
return exec.Command(binary, "client", "--host", "127.0.0.1", "--port", clickhouseTCPPort,
294+
"--password", clickhousePassword, "-q", "SELECT 1").Run() == nil
295+
}

0 commit comments

Comments
 (0)