diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5959992750..d225b6c8b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,12 @@ jobs: env: CGO_ENABLED: "0" + - name: install databases + run: go run ./cmd/sqlc-test-setup install + + - name: start databases + run: go run ./cmd/sqlc-test-setup start + - name: test ./... run: gotestsum --junitfile junit.xml -- --tags=examples -timeout 20m ./... if: ${{ matrix.os }} != "windows-2022" diff --git a/CLAUDE.md b/CLAUDE.md index 43abb0d491..f480fc1f29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,136 +10,74 @@ This document provides essential information for working with the sqlc codebase, - **Docker & Docker Compose** - Required for integration tests with databases (local development) - **Git** - For version control -## Claude Code Remote Environment Setup +## Database Setup with sqlc-test-setup -When running in the Claude Code remote environment (or any environment without Docker), you can install PostgreSQL and MySQL natively. The test framework automatically detects and uses native database installations. +The `sqlc-test-setup` tool (`cmd/sqlc-test-setup/`) automates installing and starting PostgreSQL and MySQL for tests. Both commands are idempotent and safe to re-run. -### Step 1: Configure apt Proxy (Required in Remote Environment) - -The Claude Code remote environment requires an HTTP proxy for apt. Configure it: - -```bash -bash -c 'echo "Acquire::http::Proxy \"$http_proxy\";"' | sudo tee /etc/apt/apt.conf.d/99proxy -``` - -### Step 2: Install PostgreSQL +### Install databases ```bash -sudo apt-get update -sudo apt-get install -y postgresql -sudo service postgresql start +go run ./cmd/sqlc-test-setup install ``` -Configure PostgreSQL for password authentication: - -```bash -# Set password for postgres user -sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" - -# Enable password authentication for localhost -echo 'host all all 127.0.0.1/32 md5' | sudo tee -a /etc/postgresql/16/main/pg_hba.conf -sudo service postgresql reload -``` +This will: +- Configure the apt proxy (if `http_proxy` is set, e.g. in Claude Code remote environments) +- Install PostgreSQL via apt +- Download and install MySQL 9 from Oracle's deb bundle +- Resolve all dependencies automatically +- Skip anything already installed -Test the connection: +### Start databases ```bash -PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres -c "SELECT 1;" +go run ./cmd/sqlc-test-setup start ``` -### Step 3: Install MySQL 9 - -MySQL 9 is required for full test compatibility (includes VECTOR type support). Download and install from Oracle: +This will: +- Start PostgreSQL and configure password auth (`postgres`/`postgres`) +- Start MySQL via `mysqld_safe` and set root password (`mysecretpassword`) +- Verify both connections +- Skip steps that are already done (running services, existing config) -```bash -# Download MySQL 9 bundle -curl -LO https://dev.mysql.com/get/Downloads/MySQL-9.1/mysql-server_9.1.0-1ubuntu24.04_amd64.deb-bundle.tar - -# Extract packages -mkdir -p /tmp/mysql9 -tar -xf mysql-server_9.1.0-1ubuntu24.04_amd64.deb-bundle.tar -C /tmp/mysql9 - -# Install packages (in order) -cd /tmp/mysql9 -sudo dpkg -i mysql-common_*.deb \ - mysql-community-client-plugins_*.deb \ - mysql-community-client-core_*.deb \ - mysql-community-client_*.deb \ - mysql-client_*.deb \ - mysql-community-server-core_*.deb \ - mysql-community-server_*.deb \ - mysql-server_*.deb - -# Make init script executable -sudo chmod +x /etc/init.d/mysql - -# Initialize data directory and start MySQL -sudo mysqld --initialize-insecure --user=mysql -sudo /etc/init.d/mysql start - -# Set root password -mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword'; FLUSH PRIVILEGES;" -``` +Connection URIs after start: +- PostgreSQL: `postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable` +- MySQL: `root:mysecretpassword@tcp(127.0.0.1:3306)/mysql` -Test the connection: +### Run tests ```bash -mysql -h 127.0.0.1 -u root -pmysecretpassword -e "SELECT VERSION();" -``` - -### Step 4: Run End-to-End Tests - -With both databases running, the test framework automatically detects them: - -```bash -# Run all end-to-end tests -go test --tags=examples -timeout 20m ./internal/endtoend/... - -# Run example tests -go test --tags=examples -timeout 20m ./examples/... - -# Run the full test suite +# Full test suite (requires databases running) go test --tags=examples -timeout 20m ./... ``` -The native database support (in `internal/sqltest/native/`) automatically: -- Detects running PostgreSQL and MySQL instances -- Starts services if installed but not running -- Uses standard connection URIs: - - PostgreSQL: `postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable` - - MySQL: `root:mysecretpassword@tcp(127.0.0.1:3306)/mysql` - -### Running Tests +## Running Tests -#### Basic Unit Tests (No Database Required) +### Basic Unit Tests (No Database Required) ```bash -# Simplest approach - runs all unit tests go test ./... - -# Using make -make test ``` -#### Full Test Suite with Integration Tests +### Full Test Suite with Docker (Local Development) ```bash -# Step 1: Start database containers docker compose up -d - -# Step 2: Run all tests including examples go test --tags=examples -timeout 20m ./... +``` + +### Full Test Suite without Docker (Remote / CI) -# Or use make for the full CI suite -make test-ci +```bash +go run ./cmd/sqlc-test-setup install +go run ./cmd/sqlc-test-setup start +go test --tags=examples -timeout 20m ./... ``` -#### Running Specific Tests +### Running Specific Tests ```bash # Test a specific package go test ./internal/config -go test ./internal/compiler # Run with verbose output go test -v ./internal/config @@ -193,21 +131,6 @@ The `docker-compose.yml` provides test databases: - Password: `mysecretpassword` - Database: `dinotest` -### Managing Databases - -```bash -# Start databases -make start -# or -docker compose up -d - -# Stop databases -docker compose down - -# View logs -docker compose logs -f -``` - ## Makefile Targets ```bash @@ -228,19 +151,6 @@ make start # Start database containers - **Test Command:** `gotestsum --junitfile junit.xml -- --tags=examples -timeout 20m ./...` - **Additional Checks:** `govulncheck` for vulnerability scanning -### Running Tests Like CI Locally - -```bash -# Install CI tools (optional) -go install gotest.tools/gotestsum@latest - -# Run tests with same timeout as CI -go test --tags=examples -timeout 20m ./... - -# Or use the CI make target -make test-ci -``` - ## Development Workflow ### Building Development Versions @@ -255,37 +165,18 @@ go build -o ~/go/bin/sqlc-gen-json ./cmd/sqlc-gen-json ### Environment Variables for Tests -You can customize database connections: - -**PostgreSQL:** -```bash -PG_HOST=127.0.0.1 -PG_PORT=5432 -PG_USER=postgres -PG_PASSWORD=mysecretpassword -PG_DATABASE=dinotest -``` - -**MySQL:** -```bash -MYSQL_HOST=127.0.0.1 -MYSQL_PORT=3306 -MYSQL_USER=root -MYSQL_ROOT_PASSWORD=mysecretpassword -MYSQL_DATABASE=dinotest -``` +You can override database connections via environment variables: -**Example:** ```bash -POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postgres" \ - go test -v ./... +POSTGRESQL_SERVER_URI="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" +MYSQL_SERVER_URI="root:mysecretpassword@tcp(127.0.0.1:3306)/mysql?multiStatements=true&parseTime=true" ``` ## Code Structure ### Key Directories -- `/cmd/` - Main binaries (sqlc, sqlc-gen-json) +- `/cmd/` - Main binaries (sqlc, sqlc-gen-json, sqlc-test-setup) - `/internal/cmd/` - Command implementations (vet, generate, etc.) - `/internal/engine/` - Database engine implementations - `/postgresql/` - PostgreSQL parser and converter @@ -295,6 +186,7 @@ POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postg - `/internal/codegen/` - Code generation for different languages - `/internal/config/` - Configuration file parsing - `/internal/endtoend/` - End-to-end tests +- `/internal/sqltest/` - Test database setup (Docker, native, local detection) - `/examples/` - Example projects for testing ### Important Files @@ -302,13 +194,12 @@ POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postg - `/Makefile` - Build and test targets - `/docker-compose.yml` - Database services for testing - `/.github/workflows/ci.yml` - CI configuration -- `/docs/guides/development.md` - Developer documentation ## Common Issues & Solutions ### Network Connectivity Issues -If you see errors about `storage.googleapis.com`, the Go proxy may be unreachable. Tests may still pass for packages that don't require network dependencies. +If you see errors about `storage.googleapis.com`, the Go proxy may be unreachable. Use `GOPROXY=direct go mod download` to fetch modules directly from source. ### Test Timeouts @@ -326,19 +217,23 @@ go test -race ./... ### Database Connection Failures -Ensure Docker containers are running: +If using Docker: ```bash docker compose ps docker compose up -d ``` +If using sqlc-test-setup: +```bash +go run ./cmd/sqlc-test-setup start +``` + ## Tips for Contributors -1. **Run tests before committing:** `make test-ci` +1. **Run tests before committing:** `go test --tags=examples -timeout 20m ./...` 2. **Check for race conditions:** Use `-race` flag when testing concurrent code 3. **Use specific package tests:** Faster iteration during development -4. **Start databases early:** `docker compose up -d` before running integration tests -5. **Read existing tests:** Good examples in `/internal/engine/postgresql/*_test.go` +4. **Read existing tests:** Good examples in `/internal/engine/postgresql/*_test.go` ## Git Workflow @@ -350,34 +245,18 @@ docker compose up -d ### Committing Changes ```bash -# Stage changes git add - -# Commit with descriptive message -git commit -m "Brief description - -Detailed explanation of changes. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) - -Co-Authored-By: Claude " - -# Push to remote +git commit -m "Brief description of changes" git push -u origin ``` ### Rebasing ```bash -# Update main git checkout main git pull origin main - -# Rebase feature branch git checkout git rebase main - -# Force push rebased branch git push --force-with-lease origin ``` @@ -387,21 +266,3 @@ git push --force-with-lease origin - **Development Guide:** `/docs/guides/development.md` - **CI Configuration:** `/.github/workflows/ci.yml` - **Docker Compose:** `/docker-compose.yml` - -## Recent Fixes & Improvements - -### Fixed Issues - -1. **Typo in create_function_stmt.go** - Fixed "Undertand" → "Understand" -2. **Race condition in vet.go** - Fixed Client initialization using `sync.Once` -3. **Nil pointer dereference in parse.go** - Fixed unsafe type assertion in primary key parsing - -These fixes demonstrate common patterns: -- Using `sync.Once` for thread-safe lazy initialization -- Using comma-ok idiom for safe type assertions: `if val, ok := x.(Type); ok { ... }` -- Adding proper nil checks and defensive programming - ---- - -**Last Updated:** 2025-10-21 -**Maintainer:** Claude Code diff --git a/cmd/sqlc-test-setup/main.go b/cmd/sqlc-test-setup/main.go new file mode 100644 index 0000000000..64853088a9 --- /dev/null +++ b/cmd/sqlc-test-setup/main.go @@ -0,0 +1,381 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func main() { + log.SetFlags(log.Ltime) + log.SetPrefix("[sqlc-test-setup] ") + + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "usage: sqlc-test-setup ") + os.Exit(1) + } + + switch os.Args[1] { + case "install": + if err := runInstall(); err != nil { + log.Fatalf("install failed: %s", err) + } + case "start": + if err := runStart(); err != nil { + log.Fatalf("start failed: %s", err) + } + default: + fmt.Fprintf(os.Stderr, "unknown command: %s\nusage: sqlc-test-setup \n", os.Args[1]) + os.Exit(1) + } +} + +// run executes a command with verbose logging, streaming output to stderr. +func run(name string, args ...string) error { + log.Printf("exec: %s %s", name, strings.Join(args, " ")) + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} + +// runOutput executes a command and returns its combined output. +func runOutput(name string, args ...string) (string, error) { + log.Printf("exec: %s %s", name, strings.Join(args, " ")) + cmd := exec.Command(name, args...) + out, err := cmd.CombinedOutput() + return string(out), err +} + +// commandExists checks if a binary is available in PATH. +func commandExists(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} + +// ---- install ---- + +func runInstall() error { + log.Println("=== Installing PostgreSQL and MySQL for test setup ===") + + if err := installAptProxy(); err != nil { + return fmt.Errorf("configuring apt proxy: %w", err) + } + + if err := installPostgreSQL(); err != nil { + return fmt.Errorf("installing postgresql: %w", err) + } + + if err := installMySQL(); err != nil { + return fmt.Errorf("installing mysql: %w", err) + } + + log.Println("=== Install complete ===") + return nil +} + +func installAptProxy() error { + proxy := os.Getenv("http_proxy") + if proxy == "" { + log.Println("http_proxy is not set, skipping apt proxy configuration") + return nil + } + + const confPath = "/etc/apt/apt.conf.d/99proxy" + if _, err := os.Stat(confPath); err == nil { + log.Printf("apt proxy config already exists at %s, skipping", confPath) + return nil + } + + log.Printf("configuring apt proxy to use %s", proxy) + proxyConf := fmt.Sprintf("Acquire::http::Proxy \"%s\";", proxy) + cmd := fmt.Sprintf("echo '%s' | sudo tee /etc/apt/apt.conf.d/99proxy", proxyConf) + return run("bash", "-c", cmd) +} + +func installPostgreSQL() error { + log.Println("--- Installing PostgreSQL ---") + + if commandExists("psql") { + out, err := runOutput("psql", "--version") + if err == nil { + log.Printf("postgresql is already installed: %s", strings.TrimSpace(out)) + log.Println("skipping postgresql installation") + return nil + } + } + + log.Println("updating apt package lists") + if err := run("sudo", "apt-get", "update", "-qq"); err != nil { + return fmt.Errorf("apt-get update: %w", err) + } + + log.Println("installing postgresql package") + if err := run("sudo", "apt-get", "install", "-y", "-qq", "postgresql"); err != nil { + return fmt.Errorf("apt-get install postgresql: %w", err) + } + + log.Println("postgresql installed successfully") + return nil +} + +func installMySQL() error { + log.Println("--- Installing MySQL 9 ---") + + if commandExists("mysqld") { + out, err := runOutput("mysqld", "--version") + if err == nil { + log.Printf("mysql is already installed: %s", strings.TrimSpace(out)) + log.Println("skipping mysql installation") + return nil + } + } + + bundleURL := "https://dev.mysql.com/get/Downloads/MySQL-9.1/mysql-server_9.1.0-1ubuntu24.04_amd64.deb-bundle.tar" + bundleTar := "/tmp/mysql-server-bundle.tar" + extractDir := "/tmp/mysql9" + + if _, err := os.Stat(bundleTar); err != nil { + log.Printf("downloading MySQL 9 bundle from %s", bundleURL) + if err := run("curl", "-L", "-o", bundleTar, bundleURL); err != nil { + return fmt.Errorf("downloading mysql bundle: %w", err) + } + } else { + log.Printf("mysql bundle already downloaded at %s, skipping download", bundleTar) + } + + log.Printf("extracting bundle to %s", extractDir) + if err := os.MkdirAll(extractDir, 0o755); err != nil { + return fmt.Errorf("creating extract dir: %w", err) + } + if err := run("tar", "-xf", bundleTar, "-C", extractDir); err != nil { + return fmt.Errorf("extracting mysql bundle: %w", err) + } + + // Install packages in dependency order using dpkg. + // Some packages may fail due to missing dependencies, which is expected. + // We fix them all at the end with apt-get install -f. + packages := []string{ + "mysql-common_*.deb", + "mysql-community-client-plugins_*.deb", + "mysql-community-client-core_*.deb", + "mysql-community-client_*.deb", + "mysql-client_*.deb", + "mysql-community-server-core_*.deb", + "mysql-community-server_*.deb", + "mysql-server_*.deb", + } + + for _, pkg := range packages { + log.Printf("installing %s (dependency errors will be fixed afterwards)", pkg) + cmd := fmt.Sprintf("sudo dpkg -i %s/%s", extractDir, pkg) + if err := run("bash", "-c", cmd); err != nil { + log.Printf("dpkg reported errors for %s (will fix with apt-get install -f)", pkg) + } + } + + log.Println("fixing missing dependencies with apt-get install -f") + if err := run("sudo", "apt-get", "install", "-f", "-y"); err != nil { + return fmt.Errorf("apt-get install -f: %w", err) + } + + log.Println("mysql 9 installed successfully") + return nil +} + +// ---- start ---- + +func runStart() error { + log.Println("=== Starting PostgreSQL and MySQL ===") + + if err := startPostgreSQL(); err != nil { + return fmt.Errorf("starting postgresql: %w", err) + } + + if err := startMySQL(); err != nil { + return fmt.Errorf("starting mysql: %w", err) + } + + log.Println("=== Both databases are running and configured ===") + log.Println("PostgreSQL: postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable") + log.Println("MySQL: root:mysecretpassword@tcp(127.0.0.1:3306)/mysql") + return nil +} + +func startPostgreSQL() error { + log.Println("--- Starting PostgreSQL ---") + + log.Println("starting postgresql service") + if err := run("sudo", "service", "postgresql", "start"); err != nil { + return fmt.Errorf("service postgresql start: %w", err) + } + + log.Println("setting password for postgres user") + if err := run("sudo", "-u", "postgres", "psql", "-c", "ALTER USER postgres PASSWORD 'postgres';"); err != nil { + return fmt.Errorf("setting postgres password: %w", err) + } + + log.Println("detecting postgresql config directory") + hbaPath, err := detectPgHBAPath() + if err != nil { + return fmt.Errorf("detecting pg_hba.conf path: %w", err) + } + + if err := ensurePgHBAEntry(hbaPath); err != nil { + return fmt.Errorf("configuring pg_hba.conf: %w", err) + } + + log.Println("reloading postgresql configuration") + if err := run("sudo", "service", "postgresql", "reload"); err != nil { + return fmt.Errorf("reloading postgresql: %w", err) + } + + log.Println("verifying postgresql connection") + if err := run("bash", "-c", "PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres -c 'SELECT 1;'"); err != nil { + return fmt.Errorf("postgresql connection test failed: %w", err) + } + + log.Println("postgresql is running and configured") + return nil +} + +// detectPgHBAPath finds the pg_hba.conf file across different PostgreSQL versions. +func detectPgHBAPath() (string, error) { + out, err := runOutput("bash", "-c", "sudo -u postgres psql -t -c 'SHOW hba_file;'") + if err != nil { + return "", fmt.Errorf("querying hba_file: %w (output: %s)", err, out) + } + path := strings.TrimSpace(out) + if path == "" { + return "", fmt.Errorf("pg_hba.conf path is empty") + } + log.Printf("found pg_hba.conf at %s", path) + return path, nil +} + +// ensurePgHBAEntry adds the md5 auth line to pg_hba.conf if it's not already present. +func ensurePgHBAEntry(hbaPath string) error { + hbaLine := "host all all 127.0.0.1/32 md5" + + out, err := runOutput("sudo", "cat", hbaPath) + if err != nil { + return fmt.Errorf("reading pg_hba.conf: %w", err) + } + + if strings.Contains(out, "127.0.0.1/32 md5") { + log.Println("md5 authentication for 127.0.0.1/32 already configured in pg_hba.conf, skipping") + return nil + } + + log.Printf("enabling md5 authentication in %s", hbaPath) + cmd := fmt.Sprintf("echo '%s' | sudo tee -a %s", hbaLine, hbaPath) + return run("bash", "-c", cmd) +} + +func startMySQL() error { + log.Println("--- Starting MySQL ---") + + // Check if MySQL is already running and accessible with the expected password + if mysqlReady() { + log.Println("mysql is already running and accepting connections") + return verifyMySQL() + } + + // Check if data directory already exists and has been initialized + if mysqlInitialized() { + log.Println("mysql data directory already initialized, skipping initialization") + } else { + log.Println("initializing mysql data directory") + if err := run("sudo", "mysqld", "--initialize-insecure", "--user=mysql"); err != nil { + return fmt.Errorf("mysqld --initialize-insecure: %w", err) + } + } + + // Ensure the run directory exists for the socket/pid file + if err := run("sudo", "mkdir", "-p", "/var/run/mysqld"); err != nil { + return fmt.Errorf("creating /var/run/mysqld: %w", err) + } + if err := run("sudo", "chown", "mysql:mysql", "/var/run/mysqld"); err != nil { + return fmt.Errorf("chowning /var/run/mysqld: %w", err) + } + + log.Println("starting mysql via mysqld_safe") + // mysqld_safe runs in the foreground, so we launch it in the background + cmd := exec.Command("sudo", "mysqld_safe", "--user=mysql") + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + return fmt.Errorf("starting mysqld_safe: %w", err) + } + + // Wait for MySQL to become ready + log.Println("waiting for mysql to accept connections") + if err := waitForMySQL(30 * time.Second); err != nil { + return fmt.Errorf("mysql did not start in time: %w", err) + } + log.Println("mysql is accepting connections") + + // Set root password. + // The debconf-based install may configure auth_socket plugin which only + // works via Unix socket. We need caching_sha2_password for TCP access. + log.Println("configuring mysql root password for TCP access") + if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT 1;"); err == nil { + log.Println("mysql root password already set to expected value, skipping") + } else { + log.Println("setting mysql root password with caching_sha2_password plugin") + // Try via socket (works when auth_socket is the plugin or password is blank) + if err := run("mysql", "-u", "root", "-e", + "ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'mysecretpassword'; FLUSH PRIVILEGES;"); err != nil { + return fmt.Errorf("setting mysql root password: %w", err) + } + } + + return verifyMySQL() +} + +// mysqlReady checks if MySQL is running and accepting connections with the expected password. +func mysqlReady() bool { + err := exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run() + return err == nil +} + +// waitForMySQL polls until MySQL accepts connections or the timeout expires. +func waitForMySQL(timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + // Try connecting without password (fresh) or with password (already configured) + if exec.Command("mysqladmin", "-u", "root", "ping").Run() == nil { + return nil + } + if exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run() == nil { + return nil + } + time.Sleep(500 * time.Millisecond) + } + return fmt.Errorf("timed out after %s waiting for mysql", timeout) +} + +func verifyMySQL() error { + log.Println("verifying mysql connection") + if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT VERSION();"); err != nil { + return fmt.Errorf("mysql connection test failed: %w", err) + } + log.Println("mysql is running and configured") + return nil +} + +// mysqlInitialized checks if the MySQL data directory has been initialized. +func mysqlInitialized() bool { + dataDir := "/var/lib/mysql" + entries, err := filepath.Glob(filepath.Join(dataDir, "*.pem")) + if err != nil { + return false + } + // MySQL creates TLS certificate files during initialization + return len(entries) > 0 +} diff --git a/internal/sqltest/docker/enabled.go b/internal/sqltest/docker/enabled.go index e17c0201b2..251ae1f332 100644 --- a/internal/sqltest/docker/enabled.go +++ b/internal/sqltest/docker/enabled.go @@ -13,5 +13,11 @@ func Installed() error { if _, err := exec.LookPath("docker"); err != nil { return fmt.Errorf("docker not found: %w", err) } + // Verify the Docker daemon is actually running and accessible. + // Without this check, tests will try Docker, fail on docker pull, + // and t.Fatal instead of falling back to native database support. + if out, err := exec.Command("docker", "info").CombinedOutput(); err != nil { + return fmt.Errorf("docker daemon not available: %w\n%s", err, out) + } return nil } diff --git a/internal/x/expander/expander_test.go b/internal/x/expander/expander_test.go index 84de74cdf3..52d62c6b5e 100644 --- a/internal/x/expander/expander_test.go +++ b/internal/x/expander/expander_test.go @@ -16,6 +16,8 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" "github.com/sqlc-dev/sqlc/internal/engine/sqlite" + "github.com/sqlc-dev/sqlc/internal/sqltest/docker" + "github.com/sqlc-dev/sqlc/internal/sqltest/native" ) // PostgreSQLColumnGetter implements ColumnGetter for PostgreSQL using pgxpool. @@ -109,14 +111,27 @@ func (g *SQLiteColumnGetter) GetColumnNames(ctx context.Context, query string) ( } func TestExpandPostgreSQL(t *testing.T) { - // Skip if no database connection available + ctx := context.Background() + uri := os.Getenv("POSTGRESQL_SERVER_URI") if uri == "" { - uri = "postgres://postgres:mysecretpassword@localhost:5432/postgres" + if err := docker.Installed(); err == nil { + u, err := docker.StartPostgreSQLServer(ctx) + if err != nil { + t.Fatal(err) + } + uri = u + } else if err := native.Supported(); err == nil { + u, err := native.StartPostgreSQLServer(ctx) + if err != nil { + t.Fatal(err) + } + uri = u + } else { + t.Skip("POSTGRESQL_SERVER_URI is empty and neither Docker nor native installation is available") + } } - ctx := context.Background() - pool, err := pgxpool.New(ctx, uri) if err != nil { t.Skipf("could not connect to database: %v", err) @@ -235,32 +250,27 @@ func TestExpandPostgreSQL(t *testing.T) { } func TestExpandMySQL(t *testing.T) { - // Get MySQL connection parameters - user := os.Getenv("MYSQL_USER") - if user == "" { - user = "root" - } - pass := os.Getenv("MYSQL_ROOT_PASSWORD") - if pass == "" { - pass = "mysecretpassword" - } - host := os.Getenv("MYSQL_HOST") - if host == "" { - host = "127.0.0.1" - } - port := os.Getenv("MYSQL_PORT") - if port == "" { - port = "3306" - } - dbname := os.Getenv("MYSQL_DATABASE") - if dbname == "" { - dbname = "dinotest" - } - - source := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?multiStatements=true&parseTime=true", user, pass, host, port, dbname) - ctx := context.Background() + source := os.Getenv("MYSQL_SERVER_URI") + if source == "" { + if err := docker.Installed(); err == nil { + u, err := docker.StartMySQLServer(ctx) + if err != nil { + t.Fatal(err) + } + source = u + } else if err := native.Supported(); err == nil { + u, err := native.StartMySQLServer(ctx) + if err != nil { + t.Fatal(err) + } + source = u + } else { + t.Skip("MYSQL_SERVER_URI is empty and neither Docker nor native installation is available") + } + } + db, err := sql.Open("mysql", source) if err != nil { t.Skipf("could not connect to MySQL: %v", err)