diff --git a/internal/db/clickhouse_impl.go b/internal/db/clickhouse_impl.go index 4ba1c85..b20a359 100644 --- a/internal/db/clickhouse_impl.go +++ b/internal/db/clickhouse_impl.go @@ -17,7 +17,7 @@ import ( "GoNavi-Wails/internal/ssh" "GoNavi-Wails/internal/utils" - _ "github.com/ClickHouse/clickhouse-go/v2" + clickhouse "github.com/ClickHouse/clickhouse-go/v2" ) const ( @@ -100,25 +100,20 @@ func applyClickHouseURI(config connection.ConnectionConfig) connection.Connectio return config } -func (c *ClickHouseDB) getDSN(config connection.ConnectionConfig) string { - u := &url.URL{ - Scheme: "clickhouse", - Host: net.JoinHostPort(config.Host, strconv.Itoa(config.Port)), - Path: "/" + strings.TrimPrefix(strings.TrimSpace(config.Database), "/"), +func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig) *clickhouse.Options { + timeout := getConnectTimeout(config) + return &clickhouse.Options{ + Addr: []string{ + net.JoinHostPort(config.Host, strconv.Itoa(config.Port)), + }, + Auth: clickhouse.Auth{ + Database: strings.TrimSpace(config.Database), + Username: strings.TrimSpace(config.User), + Password: config.Password, + }, + DialTimeout: timeout, + ReadTimeout: timeout, } - if strings.TrimSpace(config.Password) != "" { - u.User = url.UserPassword(strings.TrimSpace(config.User), config.Password) - } else { - u.User = url.User(strings.TrimSpace(config.User)) - } - - timeoutSeconds := getConnectTimeoutSeconds(config) - query := u.Query() - query.Set("dial_timeout", fmt.Sprintf("%ds", timeoutSeconds)) - query.Set("read_timeout", fmt.Sprintf("%ds", timeoutSeconds)) - query.Set("write_timeout", fmt.Sprintf("%ds", timeoutSeconds)) - u.RawQuery = query.Encode() - return u.String() } func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error { @@ -165,11 +160,7 @@ func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error { logger.Infof("ClickHouse 通过本地端口转发连接:%s -> %s:%d", forwarder.LocalAddr, config.Host, config.Port) } - dbConn, err := sql.Open("clickhouse", c.getDSN(runConfig)) - if err != nil { - return fmt.Errorf("打开数据库连接失败:%w", err) - } - c.conn = dbConn + c.conn = clickhouse.OpenDB(c.buildClickHouseOptions(runConfig)) if err := c.Ping(); err != nil { _ = c.Close() diff --git a/internal/db/dsn_test.go b/internal/db/dsn_test.go index f3d9392..87ec9f6 100644 --- a/internal/db/dsn_test.go +++ b/internal/db/dsn_test.go @@ -5,6 +5,7 @@ package db import ( "strings" "testing" + "time" "GoNavi-Wails/internal/connection" ) @@ -115,7 +116,7 @@ func TestTDengineDSN_UsesWebSocketFormat(t *testing.T) { } } -func TestClickHouseDSN_EscapesPasswordAndSetsTimeout(t *testing.T) { +func TestClickHouseOptions_UsesStructuredTimeoutAndAuth(t *testing.T) { c := &ClickHouseDB{} cfg := normalizeClickHouseConfig(connection.ConnectionConfig{ Type: "clickhouse", @@ -127,17 +128,35 @@ func TestClickHouseDSN_EscapesPasswordAndSetsTimeout(t *testing.T) { Timeout: 15, }) - dsn := c.getDSN(cfg) - if strings.Contains(dsn, cfg.Password) { - t.Fatalf("dsn 包含原始密码:%s", dsn) + opts := c.buildClickHouseOptions(cfg) + if opts == nil { + t.Fatal("options 为空") } - if !strings.Contains(dsn, "p%40ss%3Awo%2Frd") { - t.Fatalf("dsn 未正确转义密码:%s", dsn) + if len(opts.Addr) != 1 || opts.Addr[0] != "127.0.0.1:9000" { + t.Fatalf("addr 不符合预期:%v", opts.Addr) } - if !strings.Contains(dsn, "dial_timeout=15s") { - t.Fatalf("dsn 缺少 dial_timeout 参数:%s", dsn) + if opts.Auth.Username != "default" { + t.Fatalf("username 不符合预期:%s", opts.Auth.Username) } - if !strings.Contains(dsn, "/analytics") { - t.Fatalf("dsn 缺少数据库路径:%s", dsn) + if opts.Auth.Password != cfg.Password { + t.Fatalf("password 不符合预期:%s", opts.Auth.Password) + } + if opts.Auth.Database != "analytics" { + t.Fatalf("database 不符合预期:%s", opts.Auth.Database) + } + if opts.DialTimeout != 15*time.Second { + t.Fatalf("dial timeout 不符合预期:%s", opts.DialTimeout) + } + if opts.ReadTimeout != 15*time.Second { + t.Fatalf("read timeout 不符合预期:%s", opts.ReadTimeout) + } + if _, ok := opts.Settings["write_timeout"]; ok { + t.Fatalf("options 不应包含 write_timeout 设置:%v", opts.Settings) + } + if _, ok := opts.Settings["read_timeout"]; ok { + t.Fatalf("options 不应通过 settings 传递 read_timeout:%v", opts.Settings) + } + if _, ok := opts.Settings["dial_timeout"]; ok { + t.Fatalf("options 不应通过 settings 传递 dial_timeout:%v", opts.Settings) } }