feat(trino): 新增 Trino 可选驱动接入并补齐查询支持

- 后端新增 Trino 数据库实现与 optional driver-agent provider
- 前端补齐 catalog.schema 连接配置、URI 解析与能力开关
- SQL 编辑器对 Trino 禁用托管事务并补充前后端测试
This commit is contained in:
Syngnat
2026-06-21 13:54:42 +08:00
parent 99b75378c3
commit 8ea7ecc477
35 changed files with 1234 additions and 42 deletions

View File

@@ -1191,7 +1191,8 @@ const renderStep2 = () => {
dbType === "highgo" ||
dbType === "vastbase" ||
dbType === "opengauss" ||
dbType === "gaussdb") &&
dbType === "gaussdb" ||
dbType === "trino") &&
renderConfigSectionCard({
sectionKey: "service",
icon: <DatabaseOutlined />,

View File

@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import {
buildUriFromValues,
getConnectionParamsPlaceholder,
getUriPlaceholder,
parseTrinoUriToValues,
parseUriToValues,
} from './connectionModalUri';
describe('connectionModalUri trino support', () => {
it('parses catalog and schema from a Trino URI into the database field', () => {
expect(parseTrinoUriToValues('https://alice@127.0.0.1:8443?catalog=hive&schema=default&source=GoNavi&query_timeout=30s'))
.toMatchObject({
host: '127.0.0.1',
port: 8443,
user: 'alice',
database: 'hive.default',
useSSL: true,
sslMode: 'required',
connectionParams: 'source=GoNavi&query_timeout=30s',
});
});
it('routes generic URI parsing through the Trino parser', () => {
expect(parseUriToValues('http://alice@127.0.0.1:8080?catalog=iceberg&schema=ods', 'trino'))
.toMatchObject({
host: '127.0.0.1',
port: 8080,
user: 'alice',
database: 'iceberg.ods',
});
});
it('builds a Trino URI with catalog and schema in query parameters', () => {
expect(buildUriFromValues({
type: 'trino',
host: '127.0.0.1',
port: 8080,
user: 'alice',
database: 'hive.default',
connectionParams: 'query_timeout=45s',
})).toBe('http://alice@127.0.0.1:8080?query_timeout=45s&catalog=hive&schema=default&source=GoNavi');
});
it('keeps dedicated Trino placeholders concise', () => {
expect(getUriPlaceholder('trino')).toBe('http://user@127.0.0.1:8080?catalog=hive&schema=default&source=GoNavi');
expect(getConnectionParamsPlaceholder('trino', 'mysql')).toBe('session_properties=query_max_execution_time:30m&query_timeout=30s');
});
});

View File

@@ -342,9 +342,9 @@ const parseSingleHostUri = (
if (!hostList.length) {
return null;
}
const primary = parseHostPort(
hostList[0] || `localhost:${defaultPort}`,
defaultPort,
const primary = parseHostPort(
hostList[0] || `localhost:${defaultPort}`,
defaultPort,
);
return {
host: primary?.host || "localhost",
@@ -379,8 +379,71 @@ export const parseClickHouseHTTPUriToValues = (
defaultPort,
);
if (!parsed) {
return null;
}
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",
...extractSSLPathValuesFromParams(parsed.params, "clickhouse"),
connectionParams: serializeConnectionParams(parsed.params),
};
};
const splitTrinoNamespace = (
raw: unknown,
): { catalog: string; schema: string } => {
const text = String(raw || "").trim();
if (!text) {
return { catalog: "", schema: "" };
}
const [catalog, schema = ""] = text.split(".", 2);
return {
catalog: String(catalog || "").trim(),
schema: String(schema || "").trim(),
};
};
const joinTrinoNamespace = (catalog: string, schema: string) => {
const safeCatalog = String(catalog || "").trim();
const safeSchema = String(schema || "").trim();
if (!safeCatalog) return safeSchema;
if (!safeSchema) return safeCatalog;
return `${safeCatalog}.${safeSchema}`;
};
export const parseTrinoUriToValues = (
uriText: string,
): Record<string, any> | null => {
const trimmed = String(uriText || "").trim();
const parsed = parseSingleHostUri(
trimmed,
["trino", "http", "https"],
getDefaultPortByType("trino"),
);
if (!parsed) {
return null;
}
const params = new URLSearchParams(parsed.params);
const catalog = String(params.get("catalog") || "").trim();
const schema = String(params.get("schema") || "").trim();
params.delete("catalog");
params.delete("schema");
const skipVerify = normalizeUriBool(
params.get("skip_verify") || params.get("skipVerify"),
);
params.delete("skip_verify");
params.delete("skipVerify");
const namespace =
joinTrinoNamespace(catalog, schema) || String(parsed.database || "").trim();
return {
host: parsed.host,
port: parsed.port,
@@ -827,8 +890,8 @@ export const parseUriToValues = (
return {
host: primary?.host || "localhost",
port: primary?.port || defaultPort,
user: parsed.username,
password: parsed.password,
user: parsed.username,
password: parsed.password,
database: parsed.database || "",
rocketmqTopology:
topology === "cluster" || hostList.length > 1 ? "cluster" : "single",
@@ -864,11 +927,15 @@ export const parseUriToValues = (
parsed.params.get("skip_verify") || parsed.params.get("skipVerify"),
);
const timeoutValue = Number(parsed.params.get("timeout"));
return {
host: parsed.host,
port: parsed.port,
user: parsed.username,
password: parsed.password,
return {
host: parsed.host,
port: parsed.port,
user: parsed.username,
password: parsed.password,
database: parsed.database || "",
useSSL: tlsEnabled,
sslMode: tlsEnabled ? (skipVerify ? "skip-verify" : "required") : "disable",
...extractSSLPathValuesFromParams(parsed.params, type),
connectionParams: serializeConnectionParams(parsed.params),
timeout:
Number.isFinite(timeoutValue) && timeoutValue > 0
@@ -1070,10 +1137,13 @@ export const getUriPlaceholder = (dbType: string) => {
if (isMySQLCompatibleType(dbType)) {
const defaultPort = getDefaultPortByType(dbType);
const scheme =
dbType === "diros" ? "doris" : dbType === "starrocks" ? "starrocks" : dbType === "oceanbase" ? "oceanbase" : dbType === "goldendb" ? "goldendb" : "mysql";
if (dbType === "oceanbase") {
return `${scheme}://sys%40oracle001:pass@127.0.0.1:${defaultPort}?protocol=oracle`;
}
dbType === "diros" ? "doris" : dbType === "starrocks" ? "starrocks" : dbType === "oceanbase" ? "oceanbase" : dbType === "goldendb" ? "goldendb" : "mysql";
if (dbType === "oceanbase") {
return `${scheme}://sys%40oracle001:pass@127.0.0.1:${defaultPort}?protocol=oracle`;
}
return `${scheme}://user:pass@127.0.0.1:${defaultPort},127.0.0.2:${defaultPort}/db_name?topology=replica`;
}
if (isFileDatabaseType(dbType)) {
return dbType === "duckdb"
? "duckdb:///Users/name/demo.duckdb"
: "sqlite:///Users/name/demo.sqlite";
@@ -1140,10 +1210,12 @@ export const getConnectionParamsPlaceholder = (
if (isMySQLCompatibleType(dbType)) {
return "useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false";
}
switch (dbType) {
case "postgres":
case "kingbase":
case "highgo":
switch (dbType) {
case "postgres":
case "kingbase":
case "highgo":
case "vastbase":
case "opengauss":
case "gaussdb":
return "application_name=GoNavi&statement_timeout=30000";
case "oracle":
@@ -1167,9 +1239,9 @@ export const getConnectionParamsPlaceholder = (
case "tdengine":
return "timezone=Asia%2FShanghai";
case "iotdb":
return "fetchSize=1024&timeZone=Asia%2FShanghai";
case "rocketmq":
return "groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";
return "fetchSize=1024&timeZone=Asia%2FShanghai";
case "rocketmq":
return "groupId=gonavi&namespace=prod&tag=TagA&pullBatchSize=32&startOffset=latest";
case "mqtt":
return "topics=devices%2F%2B%2Ftelemetry,%24SYS%2F%23&clientId=gonavi-desktop&qos=1&cleanSession=true&fetchWaitMs=4000";
case "kafka":
@@ -1178,11 +1250,49 @@ export const buildUriFromValues = (values: any) => {
return "defaultQueue=orders.queue&exchange=events.topic&managementPathPrefix=/rabbitmq";
default:
return "key=value&another=value";
}
};
export const buildUriFromValues = (values: any) => {
const type = String(values.type || "")
}
};
export const buildUriFromValues = (values: any) => {
const type = String(values.type || "")
.trim()
.toLowerCase();
const defaultPort = getDefaultPortByType(type);
const host = String(values.host || "localhost").trim();
const port = Number(values.port || defaultPort);
const user = String(values.user || "").trim();
const password = String(values.password || "");
const database = String(values.database || "").trim();
const timeout = Number(values.timeout || 30);
const encodedAuth = user
? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ""}@`
: "";
if (type === "trino") {
const params = new URLSearchParams();
mergeConnectionParams(params, values.connectionParams);
const { catalog, schema } = splitTrinoNamespace(values.database);
if (catalog) {
params.set("catalog", catalog);
} else {
params.delete("catalog");
}
if (schema) {
params.set("schema", schema);
} else {
params.delete("schema");
}
if (!String(params.get("source") || "").trim()) {
params.set("source", "GoNavi");
}
if (values.useSSL) {
const mode = String(values.sslMode || "required")
.trim()
.toLowerCase();
if (mode === "skip-verify" || mode === "preferred") {
params.set("skip_verify", "true");
} else {
params.delete("skip_verify");
}