♻️ refactor(clickhouse): 使用结构化 Options 替代 DSN 连接构造

- 用 buildClickHouseOptions 收敛连接参数生成逻辑
- 将连接入口改为 clickhouse.OpenDB(Options)
- 清理 DSN 中的 write_timeout/read_timeout/dial_timeout 透传路径
- 同步重写 ClickHouse 相关测试断言
- refs #138
This commit is contained in:
Syngnat
2026-02-28 11:56:59 +08:00
parent 98c1600e13
commit 884d72f3d3
2 changed files with 44 additions and 34 deletions

View File

@@ -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()

View File

@@ -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)
}
}