mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-12 01:19:53 +08:00
✨ feat(elasticsearch): 新增 Elasticsearch 驱动支持
- 新增 ElasticsearchDB 实现 Database 接口全部 13 个方法 - 支持 HTTP/HTTPS 连接、Basic Auth、SSH 隧道、代理、SSL 回退 - 查询支持 JSON DSL 和 query_string 两种模式 - 元数据浏览:索引列表、mapping 字段、settings、aliases - 完整单元测试覆盖(httptest mock,33+ 子测试) - 注册为可选 Go 驱动,遵循现有 driver-agent 架构 Closes #521
This commit is contained in:
12
cmd/optional-driver-agent/provider_elasticsearch.go
Normal file
12
cmd/optional-driver-agent/provider_elasticsearch.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_elasticsearch_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "elasticsearch"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.ElasticsearchDB{}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,12 @@
|
||||
"version": "1.11.1",
|
||||
"checksumPolicy": "off",
|
||||
"downloadUrl": "builtin://activate/postgres"
|
||||
},
|
||||
"elasticsearch": {
|
||||
"engine": "go",
|
||||
"version": "8.19.0",
|
||||
"checksumPolicy": "off",
|
||||
"downloadUrl": "builtin://activate/elasticsearch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
|
||||
github.com/caretdev/go-irisnative v0.2.1
|
||||
github.com/duckdb/duckdb-go/v2 v2.5.5
|
||||
github.com/elastic/go-elasticsearch/v8 v8.19.6
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/highgo/pq-sm3 v0.0.0
|
||||
@@ -29,9 +30,11 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
23
go.sum
23
go.sum
@@ -38,7 +38,6 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -65,10 +64,19 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0 h1:KeT/2P54F0xS0S8Y3Pf+tFDg4HmBgReQMB+BMz8dDAs=
|
||||
github.com/elastic/elastic-transport-go/v8 v8.9.0/go.mod h1:ssMTvNS2hwf7CaiGsRRsx4gQHFZ/jS/DkLcISxekWzc=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.19.6 h1:4qa7ecJkr5rLsoHKIVGbaqcFt2o57CnOHQJi9Pts/rk=
|
||||
github.com/elastic/go-elasticsearch/v8 v8.19.6/go.mod h1:jeWebApE1oFEW/hKZqx/IRYmP/aa2+WMJkOfk+AduSI=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
@@ -127,7 +135,6 @@ github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxh
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -186,7 +193,6 @@ github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -204,9 +210,8 @@ github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
@@ -276,8 +281,14 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
|
||||
@@ -20,4 +20,5 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("elasticsearch"), "elasticsearch", "elastic")
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("clickhouse"), "clickhouse")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("elasticsearch"), "elasticsearch", "elastic")
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func init() {
|
||||
"iris": "src-1b072c57af08bec4",
|
||||
"mongodb": "src-57fdd8bfebdcd46e",
|
||||
"tdengine": "src-939715f94df1ec9c",
|
||||
"clickhouse": "src-482d62ed565b3e69",
|
||||
"clickhouse": "src-482d62ed565b3e69",
|
||||
"elasticsearch": "src-local",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@ var optionalGoDrivers = map[string]struct{}{
|
||||
"vastbase": {},
|
||||
"opengauss": {},
|
||||
"iris": {},
|
||||
"mongodb": {},
|
||||
"tdengine": {},
|
||||
"clickhouse": {},
|
||||
"mongodb": {},
|
||||
"tdengine": {},
|
||||
"clickhouse": {},
|
||||
"elasticsearch": {},
|
||||
}
|
||||
|
||||
// optionalDriverAgentRevisions 记录 GoNavi 对各可选 driver-agent 包装逻辑的兼容版本。
|
||||
@@ -63,6 +64,8 @@ func normalizeRuntimeDriverType(driverType string) string {
|
||||
return "opengauss"
|
||||
case "intersystems", "intersystemsiris", "inter-systems-iris", "inter-systems":
|
||||
return "iris"
|
||||
case "elastic":
|
||||
return "elasticsearch"
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
@@ -112,6 +115,8 @@ func driverDisplayName(driverType string) string {
|
||||
return "TDengine"
|
||||
case "clickhouse":
|
||||
return "ClickHouse"
|
||||
case "elasticsearch":
|
||||
return "Elasticsearch"
|
||||
default:
|
||||
return strings.ToUpper(strings.TrimSpace(driverType))
|
||||
}
|
||||
|
||||
366
internal/db/elasticsearch_helpers.go
Normal file
366
internal/db/elasticsearch_helpers.go
Normal file
@@ -0,0 +1,366 @@
|
||||
//go:build gonavi_full_drivers || gonavi_elasticsearch_driver
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
proxytunnel "GoNavi-Wails/internal/proxy"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
"github.com/elastic/go-elasticsearch/v8/esapi"
|
||||
)
|
||||
|
||||
const defaultEsPort = 9200
|
||||
|
||||
// ---- 配置规范化工具 ----
|
||||
|
||||
// normalizeElasticsearchConfig 规范化 Elasticsearch 连接配置。
|
||||
func normalizeElasticsearchConfig(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
runConfig := applyElasticsearchURI(config)
|
||||
if strings.TrimSpace(runConfig.Host) == "" {
|
||||
runConfig.Host = "localhost"
|
||||
}
|
||||
if runConfig.Port <= 0 {
|
||||
runConfig.Port = defaultEsPort
|
||||
}
|
||||
return runConfig
|
||||
}
|
||||
|
||||
// applyElasticsearchURI 从 URI 中解析并回填连接参数。
|
||||
func applyElasticsearchURI(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
uriText := strings.TrimSpace(config.URI)
|
||||
if uriText == "" {
|
||||
return config
|
||||
}
|
||||
parsed, err := url.Parse(uriText)
|
||||
if err != nil {
|
||||
return config
|
||||
}
|
||||
scheme := strings.ToLower(strings.TrimSpace(parsed.Scheme))
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return config
|
||||
}
|
||||
|
||||
if parsed.User != nil {
|
||||
if strings.TrimSpace(config.User) == "" {
|
||||
config.User = parsed.User.Username()
|
||||
}
|
||||
if pass, ok := parsed.User.Password(); ok && config.Password == "" {
|
||||
config.Password = pass
|
||||
}
|
||||
}
|
||||
|
||||
if scheme == "https" {
|
||||
config.UseSSL = true
|
||||
if strings.TrimSpace(config.SSLMode) == "" {
|
||||
config.SSLMode = "required"
|
||||
}
|
||||
}
|
||||
|
||||
if host := strings.TrimSpace(parsed.Host); host != "" {
|
||||
if strings.TrimSpace(config.Host) == "" || config.Host == "localhost" {
|
||||
h, port, ok := parseHostPortWithDefault(host, defaultEsPort)
|
||||
if ok {
|
||||
config.Host = h
|
||||
config.Port = port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ---- 通用判断工具 ----
|
||||
|
||||
// isHiddenIndex 判断是否为 ES 隐藏索引(以 . 开头)。
|
||||
func isHiddenIndex(name string) bool {
|
||||
return strings.HasPrefix(name, ".")
|
||||
}
|
||||
|
||||
// isJSONDSL 判断输入是否为 JSON DSL 格式。
|
||||
func isJSONDSL(query string) bool {
|
||||
return strings.HasPrefix(query, "{")
|
||||
}
|
||||
|
||||
// resolveEsIndexName 从 dbName / tableName / 默认值中确定索引名。
|
||||
func resolveEsIndexName(dbName, tableName, defaultDB string) string {
|
||||
if name := strings.TrimSpace(tableName); name != "" {
|
||||
return name
|
||||
}
|
||||
if name := strings.TrimSpace(dbName); name != "" {
|
||||
return name
|
||||
}
|
||||
return strings.TrimSpace(defaultDB)
|
||||
}
|
||||
|
||||
// ---- ES 客户端配置 ----
|
||||
|
||||
// esSSLAttemptLabel 返回连接尝试的模式标签。
|
||||
func esSSLAttemptLabel(config connection.ConnectionConfig, fallback bool) string {
|
||||
if fallback {
|
||||
return "明文回退"
|
||||
}
|
||||
if config.UseSSL {
|
||||
return "SSL"
|
||||
}
|
||||
return "明文"
|
||||
}
|
||||
|
||||
// buildESClientConfig 从连接配置构建 ES 客户端配置。
|
||||
func buildESClientConfig(config connection.ConnectionConfig) elasticsearch.Config {
|
||||
scheme := "http"
|
||||
if config.UseSSL {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
cfg := elasticsearch.Config{
|
||||
Addresses: []string{
|
||||
fmt.Sprintf("%s://%s:%d", scheme, config.Host, config.Port),
|
||||
},
|
||||
Username: strings.TrimSpace(config.User),
|
||||
Password: config.Password,
|
||||
}
|
||||
|
||||
// TLS 配置
|
||||
tlsConfig, _ := resolveGenericTLSConfig(config)
|
||||
if tlsConfig != nil {
|
||||
cfg.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// 代理支持
|
||||
if config.UseProxy {
|
||||
transport, ok := cfg.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
proxyCfg := config.Proxy
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return proxytunnel.DialContext(ctx, proxyCfg, network, addr)
|
||||
}
|
||||
cfg.Transport = transport
|
||||
}
|
||||
|
||||
// 超时设置
|
||||
timeout := getConnectTimeout(config)
|
||||
if cfg.Transport == nil {
|
||||
cfg.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
}
|
||||
if transport, ok := cfg.Transport.(*http.Transport); ok {
|
||||
transport.ResponseHeaderTimeout = timeout
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// ---- 查询响应解析 ----
|
||||
|
||||
// esIndexInfo 用于解析 Cat Indices JSON 响应。
|
||||
type esIndexInfo struct {
|
||||
Index string `json:"index"`
|
||||
Health string `json:"health"`
|
||||
Status string `json:"status"`
|
||||
DocsCount string `json:"docs.count"`
|
||||
StoreSize string `json:"store.size"`
|
||||
}
|
||||
|
||||
// esSearchResponse 用于解析 _search API 响应。
|
||||
type esSearchResponse struct {
|
||||
Hits struct {
|
||||
Total struct {
|
||||
Value int64 `json:"value"`
|
||||
} `json:"total"`
|
||||
Hits []struct {
|
||||
Source map[string]interface{} `json:"_source"`
|
||||
Index string `json:"_index"`
|
||||
ID string `json:"_id"`
|
||||
} `json:"hits"`
|
||||
} `json:"hits"`
|
||||
}
|
||||
|
||||
// esQueryWithDSL 使用 JSON DSL 执行 _search 查询。
|
||||
func (e *ElasticsearchDB) esQueryWithDSL(ctx context.Context, dsl string) ([]map[string]interface{}, []string, error) {
|
||||
indexName := e.database
|
||||
if indexName == "" {
|
||||
indexName = "*"
|
||||
}
|
||||
|
||||
res, err := e.client.Search(
|
||||
e.client.Search.WithContext(ctx),
|
||||
e.client.Search.WithIndex(indexName),
|
||||
e.client.Search.WithBody(strings.NewReader(dsl)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Elasticsearch DSL 查询失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return e.parseSearchResponse(res)
|
||||
}
|
||||
|
||||
// esQueryWithString 使用 query_string 模式执行查询。
|
||||
func (e *ElasticsearchDB) esQueryWithString(ctx context.Context, queryStr string) ([]map[string]interface{}, []string, error) {
|
||||
indexName := e.database
|
||||
if indexName == "" {
|
||||
indexName = "*"
|
||||
}
|
||||
|
||||
dsl := fmt.Sprintf(`{"query":{"query_string":{"query":"%s"}}}`, strings.ReplaceAll(queryStr, `"`, `\"`))
|
||||
|
||||
res, err := e.client.Search(
|
||||
e.client.Search.WithContext(ctx),
|
||||
e.client.Search.WithIndex(indexName),
|
||||
e.client.Search.WithBody(strings.NewReader(dsl)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Elasticsearch 查询失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
return e.parseSearchResponse(res)
|
||||
}
|
||||
|
||||
// parseSearchResponse 解析 ES _search 响应为标准行格式。
|
||||
func (e *ElasticsearchDB) parseSearchResponse(res *esapi.Response) ([]map[string]interface{}, []string, error) {
|
||||
if res.IsError() {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return nil, nil, fmt.Errorf("Elasticsearch 查询错误:%s", string(body))
|
||||
}
|
||||
|
||||
var result esSearchResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
|
||||
return nil, nil, fmt.Errorf("解析查询结果失败:%w", err)
|
||||
}
|
||||
|
||||
columnSet := make(map[string]bool)
|
||||
data := make([]map[string]interface{}, 0, len(result.Hits.Hits))
|
||||
|
||||
for _, hit := range result.Hits.Hits {
|
||||
row := make(map[string]interface{})
|
||||
row["_index"] = hit.Index
|
||||
row["_id"] = hit.ID
|
||||
columnSet["_index"] = true
|
||||
columnSet["_id"] = true
|
||||
|
||||
for k, v := range hit.Source {
|
||||
row[k] = v
|
||||
columnSet[k] = true
|
||||
}
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
columns := make([]string, 0, len(columnSet))
|
||||
for k := range columnSet {
|
||||
columns = append(columns, k)
|
||||
}
|
||||
|
||||
return data, columns, nil
|
||||
}
|
||||
|
||||
// esFetchIndexMapping 获取索引的 mapping 定义。
|
||||
func (e *ElasticsearchDB) esFetchIndexMapping(indexName string) (map[string]interface{}, error) {
|
||||
if e.client == nil {
|
||||
return nil, fmt.Errorf("连接未打开")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := e.client.Indices.GetMapping(
|
||||
e.client.Indices.GetMapping.WithContext(ctx),
|
||||
e.client.Indices.GetMapping.WithIndex(indexName),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引 mapping 失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
return nil, fmt.Errorf("获取索引 mapping 失败:%s", res.Status())
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取 mapping 响应失败:%w", err)
|
||||
}
|
||||
|
||||
var mappingResult map[string]interface{}
|
||||
if err := json.Unmarshal(body, &mappingResult); err != nil {
|
||||
return nil, fmt.Errorf("解析 mapping 失败:%w", err)
|
||||
}
|
||||
|
||||
return mappingResult, nil
|
||||
}
|
||||
|
||||
// ---- Mapping 字段提取 ----
|
||||
|
||||
// extractColumnsFromMapping 从 mapping JSON 中提取字段定义。
|
||||
func extractColumnsFromMapping(indexName string, mapping map[string]interface{}) []connection.ColumnDefinition {
|
||||
indexData, ok := mapping[indexName].(map[string]interface{})
|
||||
if !ok {
|
||||
for _, v := range mapping {
|
||||
if data, ok := v.(map[string]interface{}); ok {
|
||||
indexData = data
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if indexData == nil {
|
||||
return []connection.ColumnDefinition{}
|
||||
}
|
||||
|
||||
mappings, ok := indexData["mappings"].(map[string]interface{})
|
||||
if !ok {
|
||||
return []connection.ColumnDefinition{}
|
||||
}
|
||||
|
||||
properties, ok := mappings["properties"].(map[string]interface{})
|
||||
if !ok {
|
||||
return []connection.ColumnDefinition{}
|
||||
}
|
||||
|
||||
columns := make([]connection.ColumnDefinition, 0, len(properties))
|
||||
for name, prop := range properties {
|
||||
colType := extractEsFieldType(prop)
|
||||
comment := ""
|
||||
if propMap, ok := prop.(map[string]interface{}); ok {
|
||||
if desc, ok := propMap["description"].(string); ok {
|
||||
comment = desc
|
||||
}
|
||||
}
|
||||
columns = append(columns, connection.ColumnDefinition{
|
||||
Name: name,
|
||||
Type: colType,
|
||||
Nullable: "YES",
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
// extractEsFieldType 从字段属性中提取类型描述。
|
||||
func extractEsFieldType(prop interface{}) string {
|
||||
propMap, ok := prop.(map[string]interface{})
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
fieldType, _ := propMap["type"].(string)
|
||||
if fieldType == "" {
|
||||
if _, ok := propMap["properties"]; ok {
|
||||
return "object"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
return fieldType
|
||||
}
|
||||
366
internal/db/elasticsearch_impl.go
Normal file
366
internal/db/elasticsearch_impl.go
Normal file
@@ -0,0 +1,366 @@
|
||||
//go:build gonavi_full_drivers || gonavi_elasticsearch_driver
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
"GoNavi-Wails/internal/ssh"
|
||||
|
||||
"github.com/elastic/go-elasticsearch/v8"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEsPingTimeout = 5 * time.Second
|
||||
defaultEsQueryTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// ElasticsearchDB 实现 Database 接口,提供 Elasticsearch 数据源连接能力。
|
||||
type ElasticsearchDB struct {
|
||||
client *elasticsearch.Client
|
||||
database string // 默认索引名
|
||||
pingTimeout time.Duration
|
||||
forwarder *ssh.LocalForwarder
|
||||
}
|
||||
|
||||
// Connect 建立到 Elasticsearch 集群的连接。
|
||||
func (e *ElasticsearchDB) Connect(config connection.ConnectionConfig) error {
|
||||
// 清理旧连接
|
||||
if e.forwarder != nil {
|
||||
_ = e.forwarder.Close()
|
||||
e.forwarder = nil
|
||||
}
|
||||
e.client = nil
|
||||
|
||||
runConfig := normalizeElasticsearchConfig(config)
|
||||
e.pingTimeout = getConnectTimeout(runConfig)
|
||||
e.database = strings.TrimSpace(runConfig.Database)
|
||||
|
||||
logger.Infof("Elasticsearch 连接准备:地址=%s:%d 用户=%s SSL=%t SSH=%t 超时=%s",
|
||||
runConfig.Host, runConfig.Port, runConfig.User, runConfig.UseSSL, runConfig.UseSSH, e.pingTimeout)
|
||||
|
||||
// SSH 隧道支持
|
||||
if runConfig.UseSSH {
|
||||
logger.Infof("Elasticsearch 使用 SSH 连接:地址=%s:%d", runConfig.Host, runConfig.Port)
|
||||
forwarder, err := ssh.GetOrCreateLocalForwarder(runConfig.SSH, runConfig.Host, runConfig.Port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 SSH 隧道失败:%w", err)
|
||||
}
|
||||
e.forwarder = forwarder
|
||||
|
||||
host, portStr, err := net.SplitHostPort(forwarder.LocalAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析本地转发地址失败:%w", err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析本地端口失败:%w", err)
|
||||
}
|
||||
|
||||
runConfig.Host = host
|
||||
runConfig.Port = port
|
||||
runConfig.UseSSH = false
|
||||
logger.Infof("Elasticsearch 通过本地端口转发连接:%s -> %s:%d", forwarder.LocalAddr, config.Host, config.Port)
|
||||
}
|
||||
|
||||
// SSL 回退尝试
|
||||
attempts := []connection.ConnectionConfig{runConfig}
|
||||
if shouldTrySSLPreferredFallback(runConfig) {
|
||||
attempts = append(attempts, withSSLDisabled(runConfig))
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for idx, attempt := range attempts {
|
||||
sslLabel := esSSLAttemptLabel(attempt, idx > 0)
|
||||
logger.Infof("Elasticsearch 连接尝试:%d/%d 模式=%s 地址=%s:%d",
|
||||
idx+1, len(attempts), sslLabel, attempt.Host, attempt.Port)
|
||||
|
||||
esCfg := buildESClientConfig(attempt)
|
||||
client, err := elasticsearch.NewClient(esCfg)
|
||||
if err != nil {
|
||||
logger.Warnf("Elasticsearch 创建客户端失败:%d/%d 模式=%s 错误=%v", idx+1, len(attempts), sslLabel, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
e.client = client
|
||||
if err := e.Ping(); err != nil {
|
||||
e.client = nil
|
||||
logger.Warnf("Elasticsearch 连接验证失败:%d/%d 模式=%s 错误=%v", idx+1, len(attempts), sslLabel, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("Elasticsearch 连接成功:%d/%d 模式=%s", idx+1, len(attempts), sslLabel)
|
||||
if idx > 0 {
|
||||
logger.Warnf("Elasticsearch SSL 优先连接失败,已回退至明文连接")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("Elasticsearch 连接失败:%w", lastErr)
|
||||
}
|
||||
return fmt.Errorf("Elasticsearch 连接失败:无可用连接方案")
|
||||
}
|
||||
|
||||
// Close 关闭 Elasticsearch 连接。
|
||||
func (e *ElasticsearchDB) Close() error {
|
||||
if e.forwarder != nil {
|
||||
if err := e.forwarder.Close(); err != nil {
|
||||
logger.Warnf("关闭 Elasticsearch SSH 端口转发失败:%v", err)
|
||||
}
|
||||
e.forwarder = nil
|
||||
}
|
||||
e.client = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping 检测 Elasticsearch 连通性。
|
||||
func (e *ElasticsearchDB) Ping() error {
|
||||
if e.client == nil {
|
||||
return fmt.Errorf("连接未打开")
|
||||
}
|
||||
timeout := e.pingTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultEsPingTimeout
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
res, err := e.client.Ping(e.client.Ping.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
return fmt.Errorf("Elasticsearch Ping 失败:%s", res.Status())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query 执行 Elasticsearch 查询,支持 JSON DSL 和 query_string 两种模式。
|
||||
func (e *ElasticsearchDB) Query(query string) ([]map[string]interface{}, []string, error) {
|
||||
if e.client == nil {
|
||||
return nil, nil, fmt.Errorf("连接未打开")
|
||||
}
|
||||
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return nil, nil, fmt.Errorf("查询语句不能为空")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultEsQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
if isJSONDSL(query) {
|
||||
return e.esQueryWithDSL(ctx, query)
|
||||
}
|
||||
return e.esQueryWithString(ctx, query)
|
||||
}
|
||||
|
||||
// Exec 不支持 Elasticsearch 非查询语句执行。
|
||||
func (e *ElasticsearchDB) Exec(query string) (int64, error) {
|
||||
return 0, fmt.Errorf("Elasticsearch 不支持执行非查询语句")
|
||||
}
|
||||
|
||||
// GetDatabases 列出所有 Elasticsearch 索引(排除隐藏索引)。
|
||||
func (e *ElasticsearchDB) GetDatabases() ([]string, error) {
|
||||
if e.client == nil {
|
||||
return nil, fmt.Errorf("连接未打开")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := e.client.Cat.Indices(
|
||||
e.client.Cat.Indices.WithContext(ctx),
|
||||
e.client.Cat.Indices.WithFormat("json"),
|
||||
e.client.Cat.Indices.WithH("index"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引列表失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
return nil, fmt.Errorf("获取索引列表失败:%s", res.Status())
|
||||
}
|
||||
|
||||
var indices []struct {
|
||||
Index string `json:"index"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&indices); err != nil {
|
||||
return nil, fmt.Errorf("解析索引列表失败:%w", err)
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(indices))
|
||||
for _, idx := range indices {
|
||||
name := strings.TrimSpace(idx.Index)
|
||||
if name != "" && !isHiddenIndex(name) {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetTables 对 ES 而言索引即表,返回索引自身名称。
|
||||
func (e *ElasticsearchDB) GetTables(dbName string) ([]string, error) {
|
||||
target := strings.TrimSpace(dbName)
|
||||
if target == "" {
|
||||
target = e.database
|
||||
}
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("未指定索引名")
|
||||
}
|
||||
return []string{target}, nil
|
||||
}
|
||||
|
||||
// GetCreateStatement 返回索引的 settings + mappings 组合 JSON。
|
||||
func (e *ElasticsearchDB) GetCreateStatement(dbName, tableName string) (string, error) {
|
||||
if e.client == nil {
|
||||
return "", fmt.Errorf("连接未打开")
|
||||
}
|
||||
|
||||
indexName := resolveEsIndexName(dbName, tableName, e.database)
|
||||
if indexName == "" {
|
||||
return "", fmt.Errorf("未指定索引名")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := e.client.Indices.Get(
|
||||
[]string{indexName},
|
||||
e.client.Indices.Get.WithContext(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取索引定义失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
return "", fmt.Errorf("获取索引定义失败:%s", res.Status())
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取索引定义失败:%w", err)
|
||||
}
|
||||
|
||||
var pretty map[string]interface{}
|
||||
if err := json.Unmarshal(body, &pretty); err != nil {
|
||||
return string(body), nil
|
||||
}
|
||||
formatted, _ := json.MarshalIndent(pretty, "", " ")
|
||||
return string(formatted), nil
|
||||
}
|
||||
|
||||
// GetColumns 返回索引的 mapping 字段定义。
|
||||
func (e *ElasticsearchDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
|
||||
indexName := resolveEsIndexName(dbName, tableName, e.database)
|
||||
if indexName == "" {
|
||||
return nil, fmt.Errorf("未指定索引名")
|
||||
}
|
||||
|
||||
mapping, err := e.esFetchIndexMapping(indexName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractColumnsFromMapping(indexName, mapping), nil
|
||||
}
|
||||
|
||||
// GetAllColumns 返回索引的全部字段定义(带表名标识)。
|
||||
func (e *ElasticsearchDB) GetAllColumns(dbName string) ([]connection.ColumnDefinitionWithTable, error) {
|
||||
target := strings.TrimSpace(dbName)
|
||||
if target == "" {
|
||||
target = e.database
|
||||
}
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("未指定索引名")
|
||||
}
|
||||
|
||||
mapping, err := e.esFetchIndexMapping(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
columns := extractColumnsFromMapping(target, mapping)
|
||||
result := make([]connection.ColumnDefinitionWithTable, 0, len(columns))
|
||||
for _, col := range columns {
|
||||
result = append(result, connection.ColumnDefinitionWithTable{
|
||||
TableName: target,
|
||||
Name: col.Name,
|
||||
Type: col.Type,
|
||||
Comment: col.Comment,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetIndexes 返回索引的统计信息。
|
||||
func (e *ElasticsearchDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
|
||||
if e.client == nil {
|
||||
return nil, fmt.Errorf("连接未打开")
|
||||
}
|
||||
|
||||
indexName := resolveEsIndexName(dbName, tableName, e.database)
|
||||
if indexName == "" {
|
||||
return nil, fmt.Errorf("未指定索引名")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := e.client.Cat.Indices(
|
||||
e.client.Cat.Indices.WithContext(ctx),
|
||||
e.client.Cat.Indices.WithIndex(indexName),
|
||||
e.client.Cat.Indices.WithFormat("json"),
|
||||
e.client.Cat.Indices.WithH("index", "health", "status", "docs.count", "store.size"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引信息失败:%w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.IsError() {
|
||||
return nil, fmt.Errorf("获取索引信息失败:%s", res.Status())
|
||||
}
|
||||
|
||||
var info []esIndexInfo
|
||||
if err := json.NewDecoder(res.Body).Decode(&info); err != nil {
|
||||
return nil, fmt.Errorf("解析索引信息失败:%w", err)
|
||||
}
|
||||
|
||||
result := make([]connection.IndexDefinition, 0, len(info))
|
||||
for _, idx := range info {
|
||||
result = append(result, connection.IndexDefinition{
|
||||
Name: idx.Index,
|
||||
ColumnName: fmt.Sprintf("health=%s status=%s docs=%s size=%s", idx.Health, idx.Status, idx.DocsCount, idx.StoreSize),
|
||||
NonUnique: 0,
|
||||
SeqInIndex: 1,
|
||||
IndexType: "INDEX",
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetForeignKeys ES 不支持外键,返回空列表。
|
||||
func (e *ElasticsearchDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
|
||||
return []connection.ForeignKeyDefinition{}, nil
|
||||
}
|
||||
|
||||
// GetTriggers ES 不支持触发器,返回空列表。
|
||||
func (e *ElasticsearchDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
|
||||
return []connection.TriggerDefinition{}, nil
|
||||
}
|
||||
1111
internal/db/elasticsearch_impl_test.go
Normal file
1111
internal/db/elasticsearch_impl_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss iris mongodb tdengine clickhouse)
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase diros starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss iris mongodb tdengine clickhouse elasticsearch)
|
||||
OUTPUT_FILE="internal/db/driver_agent_revisions_gen.go"
|
||||
|
||||
usage() {
|
||||
|
||||
Reference in New Issue
Block a user