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

@@ -26,7 +26,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
if !isOceanBaseOracleProtocol(config) {
runConfig.Database = name
}
case "mysql", "mariadb", "goldendb", "greatdb", "gdb", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "iris", "intersystems", "intersystemsiris", "inter-systems", "inter-systems-iris", "mongodb", "tdengine", "iotdb", "clickhouse", "rabbitmq", "rabbit-mq", "rabbit_mq":
case "mysql", "mariadb", "goldendb", "greatdb", "gdb", "diros", "starrocks", "sphinx", "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb", "sqlserver", "iris", "intersystems", "intersystemsiris", "inter-systems", "inter-systems-iris", "mongodb", "tdengine", "iotdb", "clickhouse", "trino", "rabbitmq", "rabbit-mq", "rabbit_mq":
// 这些类型的 dbName 表示"数据库",需要写入连接配置以选择目标库。
runConfig.Database = name
case "dameng":
@@ -57,7 +57,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
// Elasticsearch索引名可能含多个点如 iot_pro_biz_operate_log.index.20240626
// 不能按点分割,直接返回原始数据库名和完整表名。
if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
if dbType == "elasticsearch" || dbType == "iotdb" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" || dbType == "trino" {
return rawDB, rawTable
}
@@ -116,7 +116,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
func normalizeMetadataSchemaAndTable(config connection.ConnectionConfig, dbName string, tableName string) (string, string) {
schema, table := normalizeSchemaAndTable(config, dbName, tableName)
switch resolveDDLDBType(config) {
case "rocketmq", "mqtt", "kafka", "rabbitmq":
case "rocketmq", "mqtt", "kafka", "rabbitmq", "trino":
return schema, table
case "postgres", "kingbase", "highgo", "vastbase", "opengauss", "gaussdb":
rawTable := strings.TrimSpace(tableName)

View File

@@ -50,6 +50,18 @@ func TestNormalizeSchemaAndTable_PostgresStillSplitsQualifiedName(t *testing.T)
}
}
func TestNormalizeSchemaAndTable_TrinoPreservesDottedTableName(t *testing.T) {
t.Parallel()
schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{
Type: "trino",
}, "hive.default", "daily.events.v1")
if schema != "hive.default" || table != "daily.events.v1" {
t.Fatalf("expected trino table name to stay intact, got %q.%q", schema, table)
}
}
func TestNormalizeSchemaAndTable_KingbaseNormalizesEscapedQualifiedName(t *testing.T) {
t.Parallel()
@@ -158,6 +170,18 @@ func TestNormalizeMetadataSchemaAndTable_NonPGLikeKeepsNormalBehavior(t *testing
}
}
func TestNormalizeMetadataSchemaAndTable_TrinoPreservesDottedTableName(t *testing.T) {
t.Parallel()
schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
Type: "trino",
}, "iceberg.analytics", "ods.orders.v1")
if schema != "iceberg.analytics" || table != "ods.orders.v1" {
t.Fatalf("expected trino metadata table to stay intact, got %q.%q", schema, table)
}
}
func TestNormalizeSchemaAndTable_PGLikePureTableStillSplitsKingbaseSearchPathOnlyInMetadata(t *testing.T) {
t.Parallel()
@@ -216,6 +240,19 @@ func TestNormalizeRunConfig_StarRocksUsesDatabaseFromTree(t *testing.T) {
}
}
func TestNormalizeRunConfig_TrinoUsesNamespaceFromTree(t *testing.T) {
t.Parallel()
runConfig := normalizeRunConfig(connection.ConnectionConfig{
Type: "trino",
Database: "hive.default",
}, "iceberg.analytics")
if runConfig.Database != "iceberg.analytics" {
t.Fatalf("expected trino namespace from tree, got %q", runConfig.Database)
}
}
func TestNormalizeRunConfig_GoldenDBUsesDatabaseFromTree(t *testing.T) {
t.Parallel()

View File

@@ -233,6 +233,8 @@ func defaultPortByType(driverType string) int {
return 27017
case "clickhouse":
return 9000
case "trino":
return 8080
case "highgo":
return 5866
case "iris":

View File

@@ -496,8 +496,8 @@ func normalizeSchemaAndTableByType(dbType string, dbName string, tableName strin
return rawDB, rawTable
}
// Elasticsearch / RocketMQ / MQTT / RabbitMQ / Kafka对象名可能含多个点或路径不能按点分割
if dbType == "elasticsearch" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" {
// Elasticsearch / RocketMQ / MQTT / RabbitMQ / Kafka / Trino:对象名可能含多个点或路径,不能按点分割
if dbType == "elasticsearch" || dbType == "rocketmq" || dbType == "mqtt" || dbType == "kafka" || dbType == "rabbitmq" || dbType == "trino" {
return rawDB, rawTable
}
@@ -575,12 +575,35 @@ func resolveCreateStatementTargets(config connection.ConnectionConfig, dbType st
func quoteTableIdentByType(dbType string, schema string, table string) string {
s := strings.TrimSpace(schema)
t := strings.TrimSpace(table)
if dbType == "trino" {
catalog, namespace := splitTrinoNamespace(s)
switch {
case catalog == "" && namespace == "":
return quoteIdentByType(dbType, t)
case namespace == "":
return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, catalog), quoteIdentByType(dbType, t))
default:
return fmt.Sprintf("%s.%s.%s", quoteIdentByType(dbType, catalog), quoteIdentByType(dbType, namespace), quoteIdentByType(dbType, t))
}
}
if s == "" {
return quoteIdentByType(dbType, t)
}
return fmt.Sprintf("%s.%s", quoteIdentByType(dbType, s), quoteIdentByType(dbType, t))
}
func splitTrinoNamespace(raw string) (string, string) {
text := strings.TrimSpace(raw)
if text == "" {
return "", ""
}
parts := strings.SplitN(text, ".", 2)
if len(parts) == 1 {
return strings.TrimSpace(parts[0]), ""
}
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
}
func buildRunConfigForDDL(config connection.ConnectionConfig, dbType string, dbName string) connection.ConnectionConfig {
runConfig := normalizeRunConfig(config, dbName)
if strings.EqualFold(strings.TrimSpace(config.Type), "custom") {

View File

@@ -193,6 +193,24 @@ func TestNormalizeSchemaAndTableByType_RabbitMQPreservesDottedQueueName(t *testi
}
}
func TestNormalizeSchemaAndTableByType_TrinoPreservesDottedTableName(t *testing.T) {
t.Parallel()
schema, table := normalizeSchemaAndTableByType("trino", "hive.default", "orders.events.v1")
if schema != "hive.default" || table != "orders.events.v1" {
t.Fatalf("expected trino table name to stay intact, got %q.%q", schema, table)
}
}
func TestQuoteTableIdentByType_TrinoKeepsCatalogSchemaAndDottedTable(t *testing.T) {
t.Parallel()
got := quoteTableIdentByType("trino", "hive.default", "orders.events.v1")
if got != `"hive"."default"."orders.events.v1"` {
t.Fatalf("unexpected trino quoted table: %s", got)
}
}
func TestBuildRunConfigForDDL_CustomHighGoUsesDatabase(t *testing.T) {
t.Parallel()

View File

@@ -278,6 +278,9 @@ func executeManagedSQLTransactionStatements(ctx context.Context, session db.Stat
}
func shouldUseManagedSQLTransaction(dbType string, query string) bool {
if strings.EqualFold(strings.TrimSpace(dbType), "trino") {
return false
}
statements := splitSQLStatements(query)
hasManagedWrite := false
for _, stmt := range statements {

View File

@@ -0,0 +1,14 @@
package app
import "testing"
func TestShouldUseManagedSQLTransaction_TrinoAlwaysUsesPlainExecution(t *testing.T) {
t.Parallel()
if shouldUseManagedSQLTransaction("trino", "UPDATE hive.default.orders SET status = 'done'") {
t.Fatal("expected trino DML to skip SQL editor managed transactions")
}
if shouldUseManagedSQLTransaction("trino", "BEGIN; UPDATE hive.default.orders SET status = 'done'; COMMIT;") {
t.Fatal("expected trino explicit transactions to stay unmanaged")
}
}

View File

@@ -395,7 +395,8 @@ const builtinDriverManifestJSON = `{
"tdengine": { "engine": "go", "version": "3.7.8", "checksumPolicy": "off", "downloadUrl": "builtin://activate/tdengine" },
"iotdb": { "engine": "go", "version": "1.3.7", "checksumPolicy": "off", "downloadUrl": "builtin://activate/iotdb" },
"clickhouse": { "engine": "go", "version": "2.43.1", "checksumPolicy": "off", "downloadUrl": "builtin://activate/clickhouse" },
"elasticsearch": { "engine": "go", "version": "8.19.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/elasticsearch" }
"elasticsearch": { "engine": "go", "version": "8.19.6", "checksumPolicy": "off", "downloadUrl": "builtin://activate/elasticsearch" },
"trino": { "engine": "go", "version": "0.333.0", "checksumPolicy": "off", "downloadUrl": "builtin://activate/trino" }
}
}`
@@ -462,6 +463,7 @@ var latestDriverVersionMap = map[string]string{
"iotdb": "1.3.7",
"clickhouse": "2.43.1",
"elasticsearch": "8.19.6",
"trino": "0.333.0",
"oracle": "2.9.0",
"postgres": "1.11.2",
"redis": "9.17.3",
@@ -489,6 +491,7 @@ var driverGoModulePathMap = map[string]string{
"iotdb": "github.com/apache/iotdb-client-go",
"clickhouse": "github.com/ClickHouse/clickhouse-go/v2",
"elasticsearch": "github.com/elastic/go-elasticsearch/v8",
"trino": "github.com/trinodb/trino-go-client",
}
var driverGoModuleAliasPathMap = map[string][]string{
@@ -1745,6 +1748,7 @@ func allDriverDefinitionsWithPackages(packages map[string]pinnedDriverPackage) [
buildOptionalGoDriverDefinition("iotdb", "Apache IoTDB", packages),
buildOptionalGoDriverDefinition("clickhouse", "ClickHouse", packages),
buildOptionalGoDriverDefinition("elasticsearch", "Elasticsearch", packages),
buildOptionalGoDriverDefinition("trino", "Trino", packages),
}
}
@@ -4330,6 +4334,8 @@ func optionalDriverBuildTag(driverType string, selectedVersion string) (string,
return "gonavi_clickhouse_driver", nil
case "elasticsearch":
return "gonavi_elasticsearch_driver", nil
case "trino":
return "gonavi_trino_driver", nil
default:
return "", fmt.Errorf("未配置驱动构建标签:%s", driverType)
}

View File

@@ -231,6 +231,7 @@ func optionalDriverAgentRevisionTestDrivers(t *testing.T) []string {
"iotdb",
"clickhouse",
"elasticsearch",
"trino",
}
for _, driverType := range drivers {
if db.OptionalDriverAgentRevision(driverType) == "" {

View File

@@ -504,6 +504,39 @@ func TestElasticsearchDriverDefinitionUsesOptionalAgent(t *testing.T) {
}
}
func TestTrinoDriverDefinitionUsesOptionalAgent(t *testing.T) {
definition, ok := resolveDriverDefinition("trino")
if !ok {
t.Fatal("expected trino driver definition")
}
if definition.Name != "Trino" {
t.Fatalf("unexpected trino driver name: %q", definition.Name)
}
if definition.BuiltIn {
t.Fatal("expected trino to be an optional driver agent")
}
if driverGoModulePathMap["trino"] != "github.com/trinodb/trino-go-client" {
t.Fatalf("unexpected trino go module path: %q", driverGoModulePathMap["trino"])
}
if definition.PinnedVersion != "0.333.0" {
t.Fatalf("unexpected trino definition pinned version: %q", definition.PinnedVersion)
}
if definition.DefaultDownloadURL != "builtin://activate/trino" {
t.Fatalf("unexpected trino default download URL: %q", definition.DefaultDownloadURL)
}
if latestDriverVersionMap["trino"] != "0.333.0" {
t.Fatalf("unexpected trino pinned version: %q", latestDriverVersionMap["trino"])
}
tags, err := optionalDriverBuildTags("trino", "")
if err != nil {
t.Fatalf("resolve trino build tags failed: %v", err)
}
if tags != "gonavi_trino_driver" {
t.Fatalf("unexpected trino build tag: %q", tags)
}
}
func TestIoTDBDriverDefinitionUsesOptionalAgent(t *testing.T) {
definition, ok := resolveDriverDefinition("iotdb")
if !ok {

View File

@@ -2850,6 +2850,20 @@ func quoteQualifiedIdentByType(dbType string, ident string) string {
}
dbType = resolveDDLDBType(connection.ConnectionConfig{Type: dbType})
if dbType == "trino" {
parts := strings.Split(raw, ".")
switch {
case len(parts) >= 3:
catalog := strings.TrimSpace(parts[0])
schema := strings.TrimSpace(parts[1])
table := strings.TrimSpace(strings.Join(parts[2:], "."))
if catalog != "" && schema != "" && table != "" {
return quoteIdentByType(dbType, catalog) + "." + quoteIdentByType(dbType, schema) + "." + quoteIdentByType(dbType, table)
}
case len(parts) <= 2:
return quoteIdentByType(dbType, raw)
}
}
if dbType == "kingbase" {
schema, table := db.SplitKingbaseQualifiedName(raw)
if table == "" {