snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
This commit is contained in:
203
internal/sqltest/native/mysql.go
Normal file
203
internal/sqltest/native/mysql.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var mysqlFlight singleflight.Group
|
||||
var mysqlURI string
|
||||
|
||||
// StartMySQLServer starts an existing MySQL installation natively (without Docker).
|
||||
func StartMySQLServer(ctx context.Context) (string, error) {
|
||||
if err := Supported(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if mysqlURI != "" {
|
||||
return mysqlURI, nil
|
||||
}
|
||||
value, err, _ := mysqlFlight.Do("mysql", func() (any, error) {
|
||||
uri, err := startMySQLServer(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mysqlURI = uri
|
||||
return uri, nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, ok := value.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("returned value was not a string")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func startMySQLServer(ctx context.Context) (string, error) {
|
||||
// Standard URI for test MySQL
|
||||
uri := "root:mysecretpassword@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
|
||||
|
||||
// Try to connect first - it might already be running
|
||||
if err := waitForMySQL(ctx, uri, 500*time.Millisecond); err == nil {
|
||||
slog.Info("native/mysql", "status", "already running")
|
||||
return uri, nil
|
||||
}
|
||||
|
||||
// Also try without password (default MySQL installation)
|
||||
uriNoPassword := "root@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
|
||||
if err := waitForMySQL(ctx, uriNoPassword, 500*time.Millisecond); err == nil {
|
||||
slog.Info("native/mysql", "status", "already running (no password)")
|
||||
// MySQL is running without password, try to set one
|
||||
if err := setMySQLPassword(ctx); err != nil {
|
||||
slog.Debug("native/mysql", "set-password-error", err)
|
||||
// Return without password if we can't set one
|
||||
return uriNoPassword, nil
|
||||
}
|
||||
// Try again with password
|
||||
if err := waitForMySQL(ctx, uri, 1*time.Second); err == nil {
|
||||
return uri, nil
|
||||
}
|
||||
// If password didn't work, use no password
|
||||
return uriNoPassword, nil
|
||||
}
|
||||
|
||||
// Try to start existing MySQL service (might be installed but not running)
|
||||
if _, err := exec.LookPath("mysqld"); err == nil {
|
||||
slog.Info("native/mysql", "status", "starting existing service")
|
||||
if err := startMySQLService(); err != nil {
|
||||
slog.Debug("native/mysql", "start-error", err)
|
||||
} else {
|
||||
// Wait for MySQL to be ready
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Try with password first
|
||||
if err := waitForMySQL(waitCtx, uri, 15*time.Second); err == nil {
|
||||
return uri, nil
|
||||
}
|
||||
|
||||
// Try without password
|
||||
if err := waitForMySQL(waitCtx, uriNoPassword, 15*time.Second); err == nil {
|
||||
if err := setMySQLPassword(ctx); err != nil {
|
||||
slog.Debug("native/mysql", "set-password-error", err)
|
||||
return uriNoPassword, nil
|
||||
}
|
||||
if err := waitForMySQL(ctx, uri, 1*time.Second); err == nil {
|
||||
return uri, nil
|
||||
}
|
||||
return uriNoPassword, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("MySQL is not installed or could not be started")
|
||||
}
|
||||
|
||||
func startMySQLService() error {
|
||||
// Try systemctl first
|
||||
cmd := exec.Command("sudo", "systemctl", "start", "mysql")
|
||||
if err := cmd.Run(); err == nil {
|
||||
// Give MySQL time to fully initialize
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try mysqld
|
||||
cmd = exec.Command("sudo", "systemctl", "start", "mysqld")
|
||||
if err := cmd.Run(); err == nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try service command
|
||||
cmd = exec.Command("sudo", "service", "mysql", "start")
|
||||
if err := cmd.Run(); err == nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd = exec.Command("sudo", "service", "mysqld", "start")
|
||||
if err := cmd.Run(); err == nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("could not start MySQL service")
|
||||
}
|
||||
|
||||
func setMySQLPassword(ctx context.Context) error {
|
||||
// Connect without password
|
||||
db, err := sql.Open("mysql", "root@tcp(localhost:3306)/mysql")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Set root password using mysql_native_password for broader compatibility
|
||||
_, err = db.ExecContext(ctx, "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysecretpassword';")
|
||||
if err != nil {
|
||||
// Try without specifying auth plugin
|
||||
_, err = db.ExecContext(ctx, "ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword';")
|
||||
if err != nil {
|
||||
// Try older MySQL syntax
|
||||
_, err = db.ExecContext(ctx, "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mysecretpassword');")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set MySQL password: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush privileges
|
||||
_, _ = db.ExecContext(ctx, "FLUSH PRIVILEGES;")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForMySQL(ctx context.Context, uri string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Make an immediate first attempt before waiting for the ticker
|
||||
if err := tryMySQLConnection(ctx, uri); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context cancelled: %w (last error: %v)", ctx.Err(), lastErr)
|
||||
case <-ticker.C:
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timeout waiting for MySQL (last error: %v)", lastErr)
|
||||
}
|
||||
if err := tryMySQLConnection(ctx, uri); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tryMySQLConnection(ctx context.Context, uri string) error {
|
||||
db, err := sql.Open("mysql", uri)
|
||||
if err != nil {
|
||||
slog.Debug("native/mysql", "open-attempt", err)
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
// Use a short timeout for ping to avoid hanging
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
return db.PingContext(pingCtx)
|
||||
}
|
||||
Reference in New Issue
Block a user