mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 19:19:52 +08:00
🐛 fix(clickhouse): 修复协议选择与连接错误提示
- 支持 ClickHouse 手动 HTTP/Native 协议优先级,避免 URI scheme 覆盖用户选择 - Auto 模式识别 Native/HTTP 协议误配错误并自动尝试备用协议 - 净化连接失败中的二进制乱码,补充测试连接参数校验和排查日志 - 前端表单增加 ClickHouse 协议选择并同步类型、缓存 key 与持久化兼容 Refs #425
This commit is contained in:
@@ -95,6 +95,7 @@ type ChoiceCardOption = {
|
||||
label: string;
|
||||
description?: string;
|
||||
};
|
||||
type ClickHouseProtocolChoice = "auto" | "http" | "native";
|
||||
const MAX_URI_LENGTH = 4096;
|
||||
const MAX_URI_HOSTS = 32;
|
||||
const MAX_TIMEOUT_SECONDS = 3600;
|
||||
@@ -102,6 +103,25 @@ const CONNECTION_MODAL_WIDTH = 960;
|
||||
const CONNECTION_MODAL_BODY_HEIGHT = 620;
|
||||
const STEP1_SIDEBAR_DIVIDER_DARK = "rgba(255, 255, 255, 0.16)";
|
||||
const STEP1_SIDEBAR_DIVIDER_LIGHT = "rgba(0, 0, 0, 0.08)";
|
||||
const CLICKHOUSE_PROTOCOL_OPTIONS: Array<{
|
||||
value: ClickHouseProtocolChoice;
|
||||
label: string;
|
||||
}> = [
|
||||
{ value: "auto", label: "自动" },
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "native", label: "Native" },
|
||||
];
|
||||
|
||||
const normalizeClickHouseProtocolValue = (
|
||||
value: unknown,
|
||||
): ClickHouseProtocolChoice => {
|
||||
const text = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (text === "http" || text === "https") return "http";
|
||||
if (text === "native" || text === "tcp") return "native";
|
||||
return "auto";
|
||||
};
|
||||
type ConnectionSecretKey =
|
||||
| "primaryPassword"
|
||||
| "sshPassword"
|
||||
@@ -848,9 +868,7 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDriverUnavailableReason = async (
|
||||
type: string,
|
||||
): Promise<string> => {
|
||||
const resolveDriverUnavailableReason = async (type: string): Promise<string> => {
|
||||
const normalized = normalizeDriverType(type);
|
||||
if (!normalized || normalized === "custom") {
|
||||
return "";
|
||||
@@ -1000,6 +1018,13 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeUriBool = (raw: unknown) => {
|
||||
const text = String(raw ?? "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return text === "1" || text === "true" || text === "yes" || text === "on";
|
||||
};
|
||||
|
||||
const normalizeFileDbPath = (rawPath: string): string => {
|
||||
let pathText = String(rawPath || "").trim();
|
||||
if (!pathText) {
|
||||
@@ -1117,6 +1142,44 @@ const ConnectionModal: React.FC<{
|
||||
};
|
||||
};
|
||||
|
||||
const parseClickHouseHTTPUriToValues = (
|
||||
uriText: string,
|
||||
fallbackPort?: number,
|
||||
): Record<string, any> | null => {
|
||||
const trimmed = String(uriText || "").trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const isHttps = lower.startsWith("https://");
|
||||
const isHttp = lower.startsWith("http://");
|
||||
if (!isHttp && !isHttps) {
|
||||
return null;
|
||||
}
|
||||
const defaultPort =
|
||||
Number.isFinite(Number(fallbackPort)) && Number(fallbackPort) > 0
|
||||
? Number(fallbackPort)
|
||||
: isHttps
|
||||
? 8443
|
||||
: 8123;
|
||||
const parsed = parseSingleHostUri(
|
||||
trimmed,
|
||||
[isHttps ? "https" : "http"],
|
||||
defaultPort,
|
||||
);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const skipVerify = normalizeUriBool(parsed.params.get("skip_verify"));
|
||||
return {
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
user: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database || "",
|
||||
clickHouseProtocol: "http",
|
||||
useSSL: isHttps,
|
||||
sslMode: isHttps ? (skipVerify ? "skip-verify" : "required") : "disable",
|
||||
};
|
||||
};
|
||||
|
||||
const parseUriToValues = (
|
||||
uriText: string,
|
||||
type: string,
|
||||
@@ -1337,6 +1400,13 @@ const ConnectionModal: React.FC<{
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "clickhouse") {
|
||||
const httpValues = parseClickHouseHTTPUriToValues(trimmedUri);
|
||||
if (httpValues) {
|
||||
return httpValues;
|
||||
}
|
||||
}
|
||||
|
||||
const singleHostSchemes = singleHostUriSchemesByType[type];
|
||||
if (singleHostSchemes && singleHostSchemes.length > 0) {
|
||||
const parsed = parseSingleHostUri(
|
||||
@@ -1412,6 +1482,9 @@ const ConnectionModal: React.FC<{
|
||||
parsedValues.sslMode = "disable";
|
||||
}
|
||||
} else if (type === "clickhouse") {
|
||||
parsedValues.clickHouseProtocol = normalizeClickHouseProtocolValue(
|
||||
parsed.params.get("protocol"),
|
||||
);
|
||||
const secure = String(
|
||||
parsed.params.get("secure") || parsed.params.get("tls") || "",
|
||||
)
|
||||
@@ -1707,7 +1780,18 @@ const ConnectionModal: React.FC<{
|
||||
return `${scheme}://${encodedAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`;
|
||||
}
|
||||
|
||||
const scheme = type === "postgres" ? "postgresql" : type;
|
||||
const clickHouseProtocol =
|
||||
type === "clickhouse"
|
||||
? normalizeClickHouseProtocolValue(values.clickHouseProtocol)
|
||||
: "auto";
|
||||
const scheme =
|
||||
type === "postgres"
|
||||
? "postgresql"
|
||||
: type === "clickhouse" && clickHouseProtocol === "http"
|
||||
? values.useSSL
|
||||
? "https"
|
||||
: "http"
|
||||
: type;
|
||||
const dbPath = database ? `/${encodeURIComponent(database)}` : "";
|
||||
const params = new URLSearchParams();
|
||||
if (supportsSSLForType(type) && values.useSSL) {
|
||||
@@ -1728,9 +1812,15 @@ const ConnectionModal: React.FC<{
|
||||
mode === "skip-verify" || mode === "preferred" ? "true" : "false",
|
||||
);
|
||||
} else if (type === "clickhouse") {
|
||||
params.set("secure", "true");
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
if (clickHouseProtocol === "http") {
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
} else {
|
||||
params.set("secure", "true");
|
||||
if (mode === "skip-verify" || mode === "preferred") {
|
||||
params.set("skip_verify", "true");
|
||||
}
|
||||
}
|
||||
} else if (type === "dameng") {
|
||||
const certPath = String(values.sslCertPath || "").trim();
|
||||
@@ -1761,6 +1851,9 @@ const ConnectionModal: React.FC<{
|
||||
params.set("protocol", "ws");
|
||||
}
|
||||
}
|
||||
if (type === "clickhouse" && clickHouseProtocol !== "auto") {
|
||||
params.set("protocol", clickHouseProtocol);
|
||||
}
|
||||
const query = params.toString();
|
||||
return `${scheme}://${encodedAuth}${toAddress(host, port, defaultPort)}${dbPath}${query ? `?${query}` : ""}`;
|
||||
};
|
||||
@@ -1967,6 +2060,10 @@ const ConnectionModal: React.FC<{
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
uri: config.uri || "",
|
||||
clickHouseProtocol:
|
||||
configType === "clickhouse"
|
||||
? normalizeClickHouseProtocolValue(config.clickHouseProtocol)
|
||||
: "auto",
|
||||
includeDatabases: initialValues.includeDatabases,
|
||||
includeRedisDatabases: initialValues.includeRedisDatabases,
|
||||
useSSL: !!config.useSSL,
|
||||
@@ -2285,9 +2382,7 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
if (unavailableReason) {
|
||||
message.warning(unavailableReason);
|
||||
promptInstallDriver(values.type, unavailableReason);
|
||||
@@ -2443,9 +2538,7 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
if (unavailableReason) {
|
||||
applyTestFailureFeedback(
|
||||
resolveConnectionTestFailureFeedback({
|
||||
@@ -2740,6 +2833,15 @@ const ConnectionModal: React.FC<{
|
||||
(Array.isArray(value) && value.length === 0);
|
||||
if (parsedUriValues) {
|
||||
Object.entries(parsedUriValues).forEach(([key, value]) => {
|
||||
if (
|
||||
key === "clickHouseProtocol" &&
|
||||
normalizeClickHouseProtocolValue((mergedValues as any)[key]) ===
|
||||
"auto" &&
|
||||
normalizeClickHouseProtocolValue(value) !== "auto"
|
||||
) {
|
||||
(mergedValues as any)[key] = value;
|
||||
return;
|
||||
}
|
||||
if (isEmptyField((mergedValues as any)[key])) {
|
||||
(mergedValues as any)[key] = value;
|
||||
}
|
||||
@@ -2748,6 +2850,35 @@ const ConnectionModal: React.FC<{
|
||||
|
||||
const type = String(mergedValues.type || "").toLowerCase();
|
||||
const defaultPort = getDefaultPortByType(type);
|
||||
if (type === "clickhouse") {
|
||||
const requestedProtocol = normalizeClickHouseProtocolValue(
|
||||
mergedValues.clickHouseProtocol,
|
||||
);
|
||||
const hostSchemeValues = parseClickHouseHTTPUriToValues(
|
||||
mergedValues.host,
|
||||
Number(mergedValues.port || defaultPort),
|
||||
);
|
||||
if (hostSchemeValues) {
|
||||
mergedValues.host = hostSchemeValues.host;
|
||||
mergedValues.port = hostSchemeValues.port;
|
||||
if (requestedProtocol !== "native") {
|
||||
mergedValues.clickHouseProtocol = "http";
|
||||
mergedValues.useSSL = hostSchemeValues.useSSL;
|
||||
mergedValues.sslMode = hostSchemeValues.sslMode;
|
||||
} else {
|
||||
mergedValues.clickHouseProtocol = "native";
|
||||
}
|
||||
if (isEmptyField(mergedValues.user)) {
|
||||
mergedValues.user = hostSchemeValues.user;
|
||||
}
|
||||
if (isEmptyField(mergedValues.password)) {
|
||||
mergedValues.password = hostSchemeValues.password;
|
||||
}
|
||||
if (isEmptyField(mergedValues.database)) {
|
||||
mergedValues.database = hostSchemeValues.database;
|
||||
}
|
||||
}
|
||||
}
|
||||
const isFileDbType = isFileDatabaseType(type);
|
||||
const sslCapableType = supportsSSLForType(type);
|
||||
|
||||
@@ -2990,6 +3121,10 @@ const ConnectionModal: React.FC<{
|
||||
? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
|
||||
: 0,
|
||||
uri: String(mergedValues.uri || "").trim(),
|
||||
clickHouseProtocol:
|
||||
type === "clickhouse"
|
||||
? normalizeClickHouseProtocolValue(mergedValues.clickHouseProtocol)
|
||||
: undefined,
|
||||
hosts: hosts,
|
||||
topology: topology,
|
||||
mysqlReplicaUser: mysqlReplicaUser,
|
||||
@@ -3017,7 +3152,10 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
setTypeSelectWarning(null);
|
||||
setDbType(type);
|
||||
form.setFieldsValue({ type: type });
|
||||
form.setFieldsValue({
|
||||
type: type,
|
||||
clickHouseProtocol: type === "clickhouse" ? "auto" : undefined,
|
||||
});
|
||||
|
||||
const defaultPort = getDefaultPortByType(type);
|
||||
if (type === "jvm") {
|
||||
@@ -4294,6 +4432,25 @@ const ConnectionModal: React.FC<{
|
||||
),
|
||||
})}
|
||||
|
||||
{dbType === "clickhouse" &&
|
||||
renderConfigSectionCard({
|
||||
sectionKey: "connectionMode",
|
||||
icon: <ClusterOutlined />,
|
||||
children: (
|
||||
<Form.Item
|
||||
name="clickHouseProtocol"
|
||||
label="连接协议"
|
||||
help="自动模式按 URI scheme 和常见端口判断;非标 HTTP/Native 端口可手动指定。"
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
options={CLICKHOUSE_PROTOCOL_OPTIONS}
|
||||
onChange={() => clearConnectionTestResultForChoice()}
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
})}
|
||||
|
||||
{(dbType === "postgres" ||
|
||||
dbType === "kingbase" ||
|
||||
dbType === "highgo" ||
|
||||
|
||||
@@ -187,6 +187,29 @@ describe('store appearance persistence', () => {
|
||||
expect(useStore.getState().connections[0]?.iconColor).toBe('#2f855a');
|
||||
});
|
||||
|
||||
it('normalizes ClickHouse protocol override when replacing saved connections', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
useStore.getState().replaceConnections([
|
||||
{
|
||||
id: 'clickhouse-http',
|
||||
name: 'ClickHouse HTTP',
|
||||
config: {
|
||||
id: 'clickhouse-http',
|
||||
type: 'clickhouse',
|
||||
host: 'clickhouse.local',
|
||||
port: 8125,
|
||||
user: 'default',
|
||||
clickHouseProtocol: 'https' as any,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(useStore.getState().connections[0]?.config.clickHouseProtocol).toBe(
|
||||
'http',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps legacy global proxy password during hydration until explicit cleanup', async () => {
|
||||
storage.setItem('lite-db-storage', JSON.stringify({
|
||||
state: {
|
||||
|
||||
@@ -163,6 +163,15 @@ const toTrimmedString = (value: unknown, fallback = ""): string => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const normalizeClickHouseProtocol = (
|
||||
value: unknown,
|
||||
): "auto" | "http" | "native" => {
|
||||
const text = toTrimmedString(value).toLowerCase();
|
||||
if (text === "http" || text === "https") return "http";
|
||||
if (text === "native" || text === "tcp") return "native";
|
||||
return "auto";
|
||||
};
|
||||
|
||||
const normalizePort = (value: unknown, fallbackPort: number): number => {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallbackPort;
|
||||
@@ -513,6 +522,12 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
|
||||
safeConfig.redisDB = normalizeIntegerInRange(raw.redisDB, 0, 0, 15);
|
||||
}
|
||||
|
||||
if (type === "clickhouse") {
|
||||
safeConfig.clickHouseProtocol = normalizeClickHouseProtocol(
|
||||
raw.clickHouseProtocol,
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "custom") {
|
||||
safeConfig.driver = toTrimmedString(raw.driver);
|
||||
safeConfig.dsn = toTrimmedString(raw.dsn).slice(0, MAX_URI_LENGTH);
|
||||
|
||||
@@ -297,6 +297,7 @@ export interface ConnectionConfig {
|
||||
timeout?: number;
|
||||
redisDB?: number; // Redis database index (0-15)
|
||||
uri?: string; // Connection URI for copy/paste
|
||||
clickHouseProtocol?: "auto" | "http" | "native"; // ClickHouse connection protocol override
|
||||
hosts?: string[]; // Multi-host addresses: host:port
|
||||
topology?: "single" | "replica" | "cluster";
|
||||
mysqlReplicaUser?: string;
|
||||
|
||||
@@ -39,6 +39,19 @@ describe('buildRpcConnectionConfig', () => {
|
||||
expect(result.database).toBe('app');
|
||||
});
|
||||
|
||||
it('preserves ClickHouse protocol override for RPC calls', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-clickhouse',
|
||||
type: 'clickhouse',
|
||||
host: 'clickhouse.local',
|
||||
port: 8125,
|
||||
user: 'default',
|
||||
clickHouseProtocol: 'http',
|
||||
} as any);
|
||||
|
||||
expect(result.clickHouseProtocol).toBe('http');
|
||||
});
|
||||
|
||||
it('fills default nested config blocks needed by RPC calls', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-redis',
|
||||
|
||||
@@ -670,6 +670,7 @@ export namespace connection {
|
||||
timeout?: number;
|
||||
redisDB?: number;
|
||||
uri?: string;
|
||||
clickHouseProtocol?: string;
|
||||
hosts?: string[];
|
||||
topology?: string;
|
||||
mysqlReplicaUser?: string;
|
||||
@@ -712,6 +713,7 @@ export namespace connection {
|
||||
this.timeout = source["timeout"];
|
||||
this.redisDB = source["redisDB"];
|
||||
this.uri = source["uri"];
|
||||
this.clickHouseProtocol = source["clickHouseProtocol"];
|
||||
this.hosts = source["hosts"];
|
||||
this.topology = source["topology"];
|
||||
this.mysqlReplicaUser = source["mysqlReplicaUser"];
|
||||
|
||||
@@ -467,6 +467,13 @@ func formatConnSummary(config connection.ConnectionConfig) string {
|
||||
b.WriteString(fmt.Sprintf(" 认证库=%s", strings.TrimSpace(config.AuthSource)))
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(config.Type), "clickhouse") {
|
||||
protocol := strings.ToLower(strings.TrimSpace(config.ClickHouseProtocol))
|
||||
if protocol == "" {
|
||||
protocol = "auto"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" ClickHouse协议=%s", protocol))
|
||||
}
|
||||
|
||||
if config.UseSSH {
|
||||
b.WriteString(fmt.Sprintf(" SSH=%s:%d 用户=%s", config.SSH.Host, config.SSH.Port, config.SSH.User))
|
||||
|
||||
@@ -80,3 +80,22 @@ func TestGetCacheKey_KeepDatabaseIsolation(t *testing.T) {
|
||||
t.Fatalf("expected different cache key for different database targets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCacheKey_KeepClickHouseProtocolIsolation(t *testing.T) {
|
||||
base := connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "clickhouse.local",
|
||||
Port: 8125,
|
||||
User: "default",
|
||||
Database: "default",
|
||||
ClickHouseProtocol: "native",
|
||||
}
|
||||
modified := base
|
||||
modified.ClickHouseProtocol = "http"
|
||||
|
||||
left := getCacheKey(base)
|
||||
right := getCacheKey(modified)
|
||||
if left == right {
|
||||
t.Fatalf("expected different cache key for different ClickHouse protocols")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,24 @@ func normalizeTestConnectionConfig(config connection.ConnectionConfig) connectio
|
||||
return normalized
|
||||
}
|
||||
|
||||
func validateTestConnectionInput(config connection.ConnectionConfig) error {
|
||||
dbType := strings.ToLower(strings.TrimSpace(config.Type))
|
||||
if dbType == "" {
|
||||
return fmt.Errorf("请先选择数据源类型")
|
||||
}
|
||||
if dbType == "clickhouse" && strings.TrimSpace(config.Host) == "" && strings.TrimSpace(config.URI) == "" {
|
||||
return fmt.Errorf("请填写 ClickHouse 主机地址或连接 URI")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generic DB Methods
|
||||
|
||||
func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResult {
|
||||
if err := validateTestConnectionInput(config); err != nil {
|
||||
logger.Warnf("DBConnect 参数校验失败:%s %s", err.Error(), formatConnSummary(config))
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
// 连接测试需要强制 ping,避免缓存命中但连接已失效时误判成功。
|
||||
_, err := a.getDatabaseForcePing(config)
|
||||
if err != nil {
|
||||
@@ -41,6 +56,10 @@ func (a *App) TestConnection(config connection.ConnectionConfig) connection.Quer
|
||||
testConfig := normalizeTestConnectionConfig(config)
|
||||
started := time.Now()
|
||||
logger.Infof("TestConnection 开始:%s", formatConnSummary(testConfig))
|
||||
if err := validateTestConnectionInput(testConfig); err != nil {
|
||||
logger.Warnf("TestConnection 参数校验失败:耗时=%s %s 原因=%s", time.Since(started).Round(time.Millisecond), formatConnSummary(testConfig), err.Error())
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
_, err := a.getDatabaseForcePing(testConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, "TestConnection 连接测试失败:耗时=%s %s", time.Since(started).Round(time.Millisecond), formatConnSummary(testConfig))
|
||||
|
||||
@@ -31,6 +31,26 @@ func TestNormalizeTestConnectionConfig_ZeroTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTestConnectionInput_ClickHouseRequiresTarget(t *testing.T) {
|
||||
err := validateTestConnectionInput(connection.ConnectionConfig{Type: "clickhouse"})
|
||||
if err == nil {
|
||||
t.Fatal("expected ClickHouse target validation error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "ClickHouse 主机地址") {
|
||||
t.Fatalf("unexpected validation error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTestConnectionInput_ClickHouseAllowsURI(t *testing.T) {
|
||||
err := validateTestConnectionInput(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
URI: "http://clickhouse.example.com:8125/default",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected ClickHouse URI to satisfy target validation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatConnSummary_BasicMySQL(t *testing.T) {
|
||||
cfg := connection.ConnectionConfig{
|
||||
Type: "mysql",
|
||||
|
||||
@@ -102,6 +102,7 @@ type ConnectionConfig struct {
|
||||
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds (default: 30)
|
||||
RedisDB int `json:"redisDB,omitempty"` // Redis database index (0-15)
|
||||
URI string `json:"uri,omitempty"` // Connection URI for copy/paste
|
||||
ClickHouseProtocol string `json:"clickHouseProtocol,omitempty"` // auto | http | native
|
||||
Hosts []string `json:"hosts,omitempty"` // Multi-host addresses: host:port
|
||||
Topology string `json:"topology,omitempty"` // single | replica | cluster
|
||||
MySQLReplicaUser string `json:"mysqlReplicaUser,omitempty"` // MySQL replica auth user
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
@@ -26,7 +28,11 @@ const (
|
||||
defaultClickHouseUser = "default"
|
||||
defaultClickHouseDatabase = "default"
|
||||
minClickHouseReadTimeout = 5 * time.Minute
|
||||
clickHouseHTTPPortHint = "8123/8132/8443"
|
||||
clickHouseHTTPPortHint = "8123/8125/8132/8443"
|
||||
|
||||
clickHouseProtocolAuto = "auto"
|
||||
clickHouseProtocolHTTP = "http"
|
||||
clickHouseProtocolNative = "native"
|
||||
)
|
||||
|
||||
type ClickHouseDB struct {
|
||||
@@ -38,6 +44,7 @@ type ClickHouseDB struct {
|
||||
|
||||
func normalizeClickHouseConfig(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
normalized := applyClickHouseURI(config)
|
||||
normalized = applyClickHouseHostURI(normalized)
|
||||
if strings.TrimSpace(normalized.Host) == "" {
|
||||
normalized.Host = "localhost"
|
||||
}
|
||||
@@ -58,15 +65,26 @@ func applyClickHouseURI(config connection.ConnectionConfig) connection.Connectio
|
||||
if uriText == "" {
|
||||
return config
|
||||
}
|
||||
lowerURI := strings.ToLower(uriText)
|
||||
if !strings.HasPrefix(lowerURI, "clickhouse://") {
|
||||
return applyClickHouseEndpointURI(config, uriText, false)
|
||||
}
|
||||
|
||||
func applyClickHouseHostURI(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
hostText := strings.TrimSpace(config.Host)
|
||||
if hostText == "" {
|
||||
return config
|
||||
}
|
||||
return applyClickHouseEndpointURI(config, hostText, true)
|
||||
}
|
||||
|
||||
func applyClickHouseEndpointURI(config connection.ConnectionConfig, uriText string, fromHostField bool) connection.ConnectionConfig {
|
||||
parsed, err := url.Parse(uriText)
|
||||
if err != nil {
|
||||
return config
|
||||
}
|
||||
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
|
||||
if !isClickHouseSupportedEndpointScheme(scheme) || strings.TrimSpace(parsed.Host) == "" {
|
||||
return config
|
||||
}
|
||||
|
||||
if parsed.User != nil {
|
||||
if strings.TrimSpace(config.User) == "" {
|
||||
@@ -85,12 +103,28 @@ func applyClickHouseURI(config connection.ConnectionConfig) connection.Connectio
|
||||
config.Database = dbName
|
||||
}
|
||||
}
|
||||
if queryProtocol := normalizeClickHouseProtocol(parsed.Query().Get("protocol")); queryProtocol != clickHouseProtocolAuto {
|
||||
config.ClickHouseProtocol = queryProtocol
|
||||
}
|
||||
endpointProtocol := normalizeClickHouseProtocol(config.ClickHouseProtocol)
|
||||
if isClickHouseHTTPURLScheme(scheme) && endpointProtocol != clickHouseProtocolNative {
|
||||
config.ClickHouseProtocol = clickHouseProtocolHTTP
|
||||
if scheme == "https" {
|
||||
config.UseSSL = true
|
||||
if normalizeSSLModeValue(config.SSLMode) == sslModeDisable || strings.TrimSpace(config.SSLMode) == "" {
|
||||
config.SSLMode = sslModeRequired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultPort := config.Port
|
||||
if defaultPort <= 0 {
|
||||
defaultPort = defaultClickHousePort
|
||||
}
|
||||
if strings.TrimSpace(config.Host) == "" {
|
||||
if isClickHouseHTTPURLScheme(scheme) && endpointProtocol != clickHouseProtocolNative && defaultPort == defaultClickHousePort {
|
||||
defaultPort = defaultClickHousePortForScheme(scheme)
|
||||
}
|
||||
if fromHostField || strings.TrimSpace(config.Host) == "" {
|
||||
host, port, ok := parseHostPortWithDefault(parsed.Host, defaultPort)
|
||||
if ok {
|
||||
config.Host = host
|
||||
@@ -103,6 +137,30 @@ func applyClickHouseURI(config connection.ConnectionConfig) connection.Connectio
|
||||
return config
|
||||
}
|
||||
|
||||
func isClickHouseSupportedEndpointScheme(scheme string) bool {
|
||||
switch scheme {
|
||||
case "clickhouse", "http", "https":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isClickHouseHTTPURLScheme(scheme string) bool {
|
||||
return scheme == "http" || scheme == "https"
|
||||
}
|
||||
|
||||
func defaultClickHousePortForScheme(scheme string) int {
|
||||
switch scheme {
|
||||
case "http":
|
||||
return 8123
|
||||
case "https":
|
||||
return 8443
|
||||
default:
|
||||
return defaultClickHousePort
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig) *clickhouse.Options {
|
||||
connectTimeout := getConnectTimeout(config)
|
||||
readTimeout := connectTimeout
|
||||
@@ -130,6 +188,15 @@ func (c *ClickHouseDB) buildClickHouseOptions(config connection.ConnectionConfig
|
||||
}
|
||||
|
||||
func detectClickHouseProtocol(config connection.ConnectionConfig) clickhouse.Protocol {
|
||||
switch normalizeClickHouseProtocol(config.ClickHouseProtocol) {
|
||||
case clickHouseProtocolHTTP:
|
||||
return clickhouse.HTTP
|
||||
case clickHouseProtocolNative:
|
||||
return clickhouse.Native
|
||||
}
|
||||
if hasClickHouseHTTPScheme(config.URI) || hasClickHouseHTTPScheme(config.Host) {
|
||||
return clickhouse.HTTP
|
||||
}
|
||||
uriText := strings.ToLower(strings.TrimSpace(config.URI))
|
||||
if strings.HasPrefix(uriText, "http://") || strings.HasPrefix(uriText, "https://") {
|
||||
return clickhouse.HTTP
|
||||
@@ -140,9 +207,25 @@ func detectClickHouseProtocol(config connection.ConnectionConfig) clickhouse.Pro
|
||||
return clickhouse.Native
|
||||
}
|
||||
|
||||
func normalizeClickHouseProtocol(raw string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case clickHouseProtocolHTTP, "https":
|
||||
return clickHouseProtocolHTTP
|
||||
case clickHouseProtocolNative, "tcp":
|
||||
return clickHouseProtocolNative
|
||||
default:
|
||||
return clickHouseProtocolAuto
|
||||
}
|
||||
}
|
||||
|
||||
func hasClickHouseHTTPScheme(raw string) bool {
|
||||
text := strings.ToLower(strings.TrimSpace(raw))
|
||||
return strings.HasPrefix(text, "http://") || strings.HasPrefix(text, "https://")
|
||||
}
|
||||
|
||||
func isClickHouseHTTPPort(port int) bool {
|
||||
switch port {
|
||||
case 8123, 8132, 8443:
|
||||
case 8123, 8125, 8132, 8443:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -159,18 +242,88 @@ func isClickHouseProtocolMismatch(err error) bool {
|
||||
}
|
||||
return strings.Contains(text, "unexpected packet [72]") ||
|
||||
(strings.Contains(text, "unexpected packet") && strings.Contains(text, "handshake")) ||
|
||||
(strings.Contains(text, "cannot parse input") && strings.Contains(text, "expected '('")) ||
|
||||
strings.Contains(text, "http response to https client") ||
|
||||
strings.Contains(text, "malformed http response")
|
||||
}
|
||||
|
||||
func clickHouseProtocolName(protocol clickhouse.Protocol) string {
|
||||
if protocol == clickhouse.HTTP {
|
||||
return "HTTP"
|
||||
}
|
||||
return "Native"
|
||||
}
|
||||
|
||||
func sanitizeClickHouseErrorMessage(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
text := strings.ToValidUTF8(err.Error(), "<22>")
|
||||
var b strings.Builder
|
||||
lastSpace := false
|
||||
for _, r := range text {
|
||||
if r == utf8.RuneError || r == '<27>' {
|
||||
if !lastSpace {
|
||||
b.WriteByte(' ')
|
||||
lastSpace = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if unicode.IsControl(r) {
|
||||
if !lastSpace {
|
||||
b.WriteByte(' ')
|
||||
lastSpace = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
b.WriteRune(r)
|
||||
lastSpace = unicode.IsSpace(r)
|
||||
}
|
||||
sanitized := strings.Join(strings.Fields(b.String()), " ")
|
||||
if len(sanitized) > 320 {
|
||||
return sanitized[:320] + "..."
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
func clickHouseAttemptFailureMessage(protocol clickhouse.Protocol, err error) string {
|
||||
if isClickHouseProtocolMismatch(err) {
|
||||
if protocol == clickhouse.Native {
|
||||
return "服务端响应不像 Native 握手,当前端口更像 HTTP/HTTPS 端口;请选择 HTTP 协议,或确认 ClickHouse Native 端口"
|
||||
}
|
||||
return "服务端响应不像 HTTP 响应,当前端口更像 Native 端口;请选择 Native 协议,或确认 ClickHouse HTTP 端口"
|
||||
}
|
||||
message := sanitizeClickHouseErrorMessage(err)
|
||||
if message == "" {
|
||||
return "未知错误"
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func clickHouseConnectFailureSummary(config connection.ConnectionConfig, failures []string) string {
|
||||
protocolMode := normalizeClickHouseProtocol(config.ClickHouseProtocol)
|
||||
detail := strings.Join(failures, ";")
|
||||
if strings.TrimSpace(detail) == "" {
|
||||
detail = "未获取到驱动返回的错误详情"
|
||||
}
|
||||
if protocolMode != clickHouseProtocolAuto {
|
||||
return fmt.Sprintf("ClickHouse 连接验证失败:已按用户选择使用 %s 协议连接 %s:%d。%s",
|
||||
strings.ToUpper(protocolMode), config.Host, config.Port, detail)
|
||||
}
|
||||
return fmt.Sprintf("ClickHouse 连接验证失败:自动协议探测未成功(Native 常见端口 9000/9440,HTTP 常见端口 %s;非标端口建议在连接协议中手动指定)。%s",
|
||||
clickHouseHTTPPortHint, detail)
|
||||
}
|
||||
|
||||
func withClickHouseProtocol(config connection.ConnectionConfig, protocol clickhouse.Protocol) connection.ConnectionConfig {
|
||||
next := config
|
||||
switch protocol {
|
||||
case clickhouse.HTTP:
|
||||
next.ClickHouseProtocol = clickHouseProtocolHTTP
|
||||
if next.Port == 0 {
|
||||
next.Port = 8123
|
||||
}
|
||||
default:
|
||||
next.ClickHouseProtocol = clickHouseProtocolNative
|
||||
if next.Port == 0 {
|
||||
next.Port = defaultClickHousePort
|
||||
}
|
||||
@@ -178,6 +331,17 @@ func withClickHouseProtocol(config connection.ConnectionConfig, protocol clickho
|
||||
return next
|
||||
}
|
||||
|
||||
func clickHouseProtocolsForAttempt(config connection.ConnectionConfig) []clickhouse.Protocol {
|
||||
primaryProtocol := detectClickHouseProtocol(config)
|
||||
if normalizeClickHouseProtocol(config.ClickHouseProtocol) != clickHouseProtocolAuto {
|
||||
return []clickhouse.Protocol{primaryProtocol}
|
||||
}
|
||||
if primaryProtocol == clickhouse.Native {
|
||||
return []clickhouse.Protocol{primaryProtocol, clickhouse.HTTP}
|
||||
}
|
||||
return []clickhouse.Protocol{primaryProtocol, clickhouse.Native}
|
||||
}
|
||||
|
||||
func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error {
|
||||
if supported, reason := DriverRuntimeSupportStatus("clickhouse"); !supported {
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
@@ -198,8 +362,14 @@ func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error {
|
||||
runConfig := normalizeClickHouseConfig(config)
|
||||
c.pingTimeout = getConnectTimeout(runConfig)
|
||||
c.database = runConfig.Database
|
||||
logger.Infof("ClickHouse 连接准备:地址=%s:%d 数据库=%s 用户=%s 协议选择=%s SSL=%t SSH=%t 超时=%s",
|
||||
runConfig.Host, runConfig.Port, runConfig.Database, runConfig.User,
|
||||
normalizeClickHouseProtocol(runConfig.ClickHouseProtocol), runConfig.UseSSL, runConfig.UseSSH, c.pingTimeout)
|
||||
|
||||
if runConfig.UseSSH {
|
||||
if normalizeClickHouseProtocol(runConfig.ClickHouseProtocol) == clickHouseProtocolAuto && detectClickHouseProtocol(runConfig) == clickhouse.HTTP {
|
||||
runConfig.ClickHouseProtocol = clickHouseProtocolHTTP
|
||||
}
|
||||
logger.Infof("ClickHouse 使用 SSH 连接:地址=%s:%d 用户=%s", runConfig.Host, runConfig.Port, runConfig.User)
|
||||
forwarder, err := ssh.GetOrCreateLocalForwarder(runConfig.SSH, runConfig.Host, runConfig.Port)
|
||||
if err != nil {
|
||||
@@ -229,19 +399,17 @@ func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error {
|
||||
|
||||
var failures []string
|
||||
for idx, attempt := range attempts {
|
||||
primaryProtocol := detectClickHouseProtocol(attempt)
|
||||
protocols := []clickhouse.Protocol{primaryProtocol}
|
||||
if primaryProtocol == clickhouse.Native {
|
||||
protocols = append(protocols, clickhouse.HTTP)
|
||||
} else {
|
||||
protocols = append(protocols, clickhouse.Native)
|
||||
}
|
||||
|
||||
protocols := clickHouseProtocolsForAttempt(attempt)
|
||||
for pIdx, protocol := range protocols {
|
||||
protocolConfig := withClickHouseProtocol(attempt, protocol)
|
||||
logger.Infof("ClickHouse 连接尝试:第%d组/%d 协议=%s 地址=%s:%d SSL=%t",
|
||||
idx+1, len(attempts), clickHouseProtocolName(protocol), protocolConfig.Host, protocolConfig.Port, protocolConfig.UseSSL)
|
||||
c.conn = clickhouse.OpenDB(c.buildClickHouseOptions(protocolConfig))
|
||||
if err := c.Ping(); err != nil {
|
||||
failures = append(failures, fmt.Sprintf("第%d次连接验证失败(protocol=%s): %v", idx+1, protocol.String(), err))
|
||||
failureMessage := clickHouseAttemptFailureMessage(protocol, err)
|
||||
failures = append(failures, fmt.Sprintf("第%d次连接验证失败(protocol=%s): %s", idx+1, protocol.String(), failureMessage))
|
||||
logger.Warnf("ClickHouse 连接尝试失败:第%d组/%d 协议=%s 地址=%s:%d SSL=%t 原因=%s",
|
||||
idx+1, len(attempts), clickHouseProtocolName(protocol), protocolConfig.Host, protocolConfig.Port, protocolConfig.UseSSL, failureMessage)
|
||||
if c.conn != nil {
|
||||
_ = c.conn.Close()
|
||||
c.conn = nil
|
||||
@@ -258,12 +426,13 @@ func (c *ClickHouseDB) Connect(config connection.ConnectionConfig) error {
|
||||
if pIdx > 0 {
|
||||
logger.Warnf("ClickHouse 已自动切换连接协议为 %s(常见于 %s HTTP 端口)", protocol.String(), clickHouseHTTPPortHint)
|
||||
}
|
||||
logger.Infof("ClickHouse 连接验证成功:协议=%s 地址=%s:%d 数据库=%s", clickHouseProtocolName(protocol), protocolConfig.Host, protocolConfig.Port, protocolConfig.Database)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.Close()
|
||||
return fmt.Errorf("连接建立后验证失败(可检查 ClickHouse 端口与协议是否匹配:Native=9000/9440,HTTP=%s):%s", clickHouseHTTPPortHint, strings.Join(failures, ";"))
|
||||
return fmt.Errorf("%s", clickHouseConnectFailureSummary(runConfig, failures))
|
||||
}
|
||||
|
||||
func (c *ClickHouseDB) Close() error {
|
||||
|
||||
@@ -160,6 +160,13 @@ func TestDetectClickHouseProtocolTreatsHTTPPortsAsHTTP(t *testing.T) {
|
||||
},
|
||||
expected: clickhouse.HTTP,
|
||||
},
|
||||
{
|
||||
name: "custom http port 8125",
|
||||
config: connection.ConnectionConfig{
|
||||
Port: 8125,
|
||||
},
|
||||
expected: clickhouse.HTTP,
|
||||
},
|
||||
{
|
||||
name: "https port",
|
||||
config: connection.ConnectionConfig{
|
||||
@@ -181,6 +188,30 @@ func TestDetectClickHouseProtocolTreatsHTTPPortsAsHTTP(t *testing.T) {
|
||||
},
|
||||
expected: clickhouse.Native,
|
||||
},
|
||||
{
|
||||
name: "host http scheme",
|
||||
config: connection.ConnectionConfig{
|
||||
Host: "http://clickhouse.example.com",
|
||||
Port: 8125,
|
||||
},
|
||||
expected: clickhouse.HTTP,
|
||||
},
|
||||
{
|
||||
name: "manual http overrides native port",
|
||||
config: connection.ConnectionConfig{
|
||||
ClickHouseProtocol: "http",
|
||||
Port: 9000,
|
||||
},
|
||||
expected: clickhouse.HTTP,
|
||||
},
|
||||
{
|
||||
name: "manual native overrides http port",
|
||||
config: connection.ConnectionConfig{
|
||||
ClickHouseProtocol: "native",
|
||||
Port: 8123,
|
||||
},
|
||||
expected: clickhouse.Native,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -192,6 +223,172 @@ func TestDetectClickHouseProtocolTreatsHTTPPortsAsHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClickHouseConfigParsesHTTPHostScheme(t *testing.T) {
|
||||
config := normalizeClickHouseConfig(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "https://clickhouse.example.com:8125/analytics",
|
||||
User: "alice",
|
||||
Password: "secret",
|
||||
})
|
||||
|
||||
if config.Host != "clickhouse.example.com" {
|
||||
t.Fatalf("expected host without scheme, got %q", config.Host)
|
||||
}
|
||||
if config.Port != 8125 {
|
||||
t.Fatalf("expected port 8125, got %d", config.Port)
|
||||
}
|
||||
if config.Database != "analytics" {
|
||||
t.Fatalf("expected database analytics, got %q", config.Database)
|
||||
}
|
||||
if config.ClickHouseProtocol != "http" {
|
||||
t.Fatalf("expected http protocol hint, got %q", config.ClickHouseProtocol)
|
||||
}
|
||||
if !config.UseSSL || config.SSLMode != sslModeRequired {
|
||||
t.Fatalf("expected https host to enable required SSL, got useSSL=%v sslMode=%q", config.UseSSL, config.SSLMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClickHouseConfigKeepsManualNativeWhenHostHasHTTPScheme(t *testing.T) {
|
||||
config := normalizeClickHouseConfig(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "http://clickhouse.example.com:9001/analytics",
|
||||
ClickHouseProtocol: "native",
|
||||
User: "alice",
|
||||
Password: "secret",
|
||||
})
|
||||
|
||||
if config.Host != "clickhouse.example.com" {
|
||||
t.Fatalf("expected host without scheme, got %q", config.Host)
|
||||
}
|
||||
if config.Port != 9001 {
|
||||
t.Fatalf("expected user-provided native port 9001, got %d", config.Port)
|
||||
}
|
||||
if config.Database != "analytics" {
|
||||
t.Fatalf("expected database analytics, got %q", config.Database)
|
||||
}
|
||||
if config.ClickHouseProtocol != "native" {
|
||||
t.Fatalf("expected manual native protocol to be preserved, got %q", config.ClickHouseProtocol)
|
||||
}
|
||||
if config.UseSSL {
|
||||
t.Fatalf("manual native protocol should not be forced to HTTP TLS by http scheme")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClickHouseConfigUsesNativeDefaultPortForManualNativeHTTPScheme(t *testing.T) {
|
||||
config := normalizeClickHouseConfig(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "https://clickhouse.example.com/analytics",
|
||||
ClickHouseProtocol: "native",
|
||||
})
|
||||
|
||||
if config.Host != "clickhouse.example.com" {
|
||||
t.Fatalf("expected host without scheme, got %q", config.Host)
|
||||
}
|
||||
if config.Port != defaultClickHousePort {
|
||||
t.Fatalf("expected native default port %d, got %d", defaultClickHousePort, config.Port)
|
||||
}
|
||||
if config.ClickHouseProtocol != "native" {
|
||||
t.Fatalf("expected manual native protocol to be preserved, got %q", config.ClickHouseProtocol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClickHouseProtocolMismatchIncludesHTTPParseBinaryResponse(t *testing.T) {
|
||||
err := errors.New("code: 27, message: Cannot parse input: expected '(' before: '\x02\x00\x01\x00'")
|
||||
if !isClickHouseProtocolMismatch(err) {
|
||||
t.Fatalf("expected binary parse response to be treated as protocol mismatch")
|
||||
}
|
||||
|
||||
message := clickHouseAttemptFailureMessage(clickhouse.Native, err)
|
||||
if !strings.Contains(message, "不像 Native") || strings.Contains(message, "\x00") {
|
||||
t.Fatalf("expected user-facing native mismatch message without binary bytes, got %q", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithClickHouseProtocolForcesProtocolSelection(t *testing.T) {
|
||||
httpConfig := withClickHouseProtocol(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "clickhouse.example.com",
|
||||
Port: 8125,
|
||||
}, clickhouse.HTTP)
|
||||
if protocol := detectClickHouseProtocol(httpConfig); protocol != clickhouse.HTTP {
|
||||
t.Fatalf("expected forced HTTP protocol, got %s", protocol.String())
|
||||
}
|
||||
|
||||
nativeConfig := withClickHouseProtocol(connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Host: "http://clickhouse.example.com",
|
||||
Port: 8125,
|
||||
}, clickhouse.Native)
|
||||
if protocol := detectClickHouseProtocol(nativeConfig); protocol != clickhouse.Native {
|
||||
t.Fatalf("expected forced Native protocol, got %s", protocol.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestClickHouseProtocolsForAttemptOnlyFallsBackInAutoMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config connection.ConnectionConfig
|
||||
expected []clickhouse.Protocol
|
||||
}{
|
||||
{
|
||||
name: "auto native falls back to http",
|
||||
config: connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Port: 9000,
|
||||
},
|
||||
expected: []clickhouse.Protocol{clickhouse.Native, clickhouse.HTTP},
|
||||
},
|
||||
{
|
||||
name: "auto http falls back to native",
|
||||
config: connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Port: 8125,
|
||||
},
|
||||
expected: []clickhouse.Protocol{clickhouse.HTTP, clickhouse.Native},
|
||||
},
|
||||
{
|
||||
name: "manual http does not try native",
|
||||
config: connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Port: 9000,
|
||||
ClickHouseProtocol: "http",
|
||||
},
|
||||
expected: []clickhouse.Protocol{clickhouse.HTTP},
|
||||
},
|
||||
{
|
||||
name: "manual native does not try http",
|
||||
config: connection.ConnectionConfig{
|
||||
Type: "clickhouse",
|
||||
Port: 8125,
|
||||
ClickHouseProtocol: "native",
|
||||
},
|
||||
expected: []clickhouse.Protocol{clickhouse.Native},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := clickHouseProtocolsForAttempt(tt.config)
|
||||
if len(got) != len(tt.expected) {
|
||||
t.Fatalf("expected protocols %v, got %v", protocolNames(tt.expected), protocolNames(got))
|
||||
}
|
||||
for idx := range got {
|
||||
if got[idx] != tt.expected[idx] {
|
||||
t.Fatalf("expected protocols %v, got %v", protocolNames(tt.expected), protocolNames(got))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func protocolNames(protocols []clickhouse.Protocol) []string {
|
||||
names := make([]string, 0, len(protocols))
|
||||
for _, protocol := range protocols {
|
||||
names = append(names, protocol.String())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
type fakeClickHouseDriver struct{}
|
||||
|
||||
func (fakeClickHouseDriver) Open(name string) (driver.Conn, error) {
|
||||
|
||||
Reference in New Issue
Block a user