mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-08 11:31:57 +08:00
12
internal/db/batch_write_interface_gaussdb_test.go
Normal file
12
internal/db/batch_write_interface_gaussdb_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_gaussdb_driver
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBatchWriteDriverCoverageGaussDB(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*GaussDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected GaussDB to implement BatchWriteExecer")
|
||||
}
|
||||
}
|
||||
12
internal/db/batch_write_interface_highgo_test.go
Normal file
12
internal/db/batch_write_interface_highgo_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_highgo_driver
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBatchWriteDriverCoverageHighGo(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*HighGoDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected HighGoDB to implement BatchWriteExecer")
|
||||
}
|
||||
}
|
||||
12
internal/db/batch_write_interface_kingbase_test.go
Normal file
12
internal/db/batch_write_interface_kingbase_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_kingbase_driver
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBatchWriteDriverCoverageKingbase(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*KingbaseDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected KingbaseDB to implement BatchWriteExecer")
|
||||
}
|
||||
}
|
||||
12
internal/db/batch_write_interface_sqlserver_test.go
Normal file
12
internal/db/batch_write_interface_sqlserver_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_sqlserver_driver
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBatchWriteDriverCoverageSQLServer(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*SqlServerDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected SqlServerDB to implement BatchWriteExecer")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build gonavi_full_drivers || gonavi_sqlserver_driver || gonavi_kingbase_driver || gonavi_highgo_driver || gonavi_vastbase_driver
|
||||
//go:build gonavi_full_drivers
|
||||
|
||||
package db
|
||||
|
||||
@@ -32,4 +32,11 @@ func TestBatchWriteDriverCoverage(t *testing.T) {
|
||||
t.Fatal("expected VastbaseDB to implement BatchWriteExecer")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("gaussdb", func(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*GaussDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected GaussDB to implement BatchWriteExecer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
12
internal/db/batch_write_interface_vastbase_test.go
Normal file
12
internal/db/batch_write_interface_vastbase_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_vastbase_driver
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBatchWriteDriverCoverageVastbase(t *testing.T) {
|
||||
var driver BatchWriteExecer = (*VastbaseDB)(nil)
|
||||
if driver == nil {
|
||||
t.Fatal("expected VastbaseDB to implement BatchWriteExecer")
|
||||
}
|
||||
}
|
||||
@@ -516,6 +516,8 @@ func normalizeDatabaseType(dbType string) string {
|
||||
return "kingbase"
|
||||
case "opengauss", "open_gauss", "open-gauss":
|
||||
return "opengauss"
|
||||
case "gaussdb", "gauss_db", "gauss-db":
|
||||
return "gaussdb"
|
||||
case "intersystems", "intersystemsiris", "inter-systems-iris", "inter-systems":
|
||||
return "iris"
|
||||
case "chromadb", "chroma-db":
|
||||
|
||||
@@ -16,6 +16,7 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("highgo"), "highgo")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("vastbase"), "vastbase")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("opengauss"), "opengauss", "open_gauss", "open-gauss")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("gaussdb"), "gaussdb", "gauss_db", "gauss-db")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("iris"), "iris", "intersystems")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine")
|
||||
|
||||
@@ -16,6 +16,7 @@ func registerOptionalDatabaseFactories() {
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("highgo"), "highgo")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("vastbase"), "vastbase")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("opengauss"), "opengauss", "open_gauss", "open-gauss")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("gaussdb"), "gaussdb", "gauss_db", "gauss-db")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("iris"), "iris", "intersystems")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("mongodb"), "mongodb")
|
||||
registerDatabaseFactory(newOptionalDriverAgentDatabase("tdengine"), "tdengine")
|
||||
|
||||
@@ -17,6 +17,7 @@ func init() {
|
||||
"highgo": "src-5a29a1d3685eb6b4",
|
||||
"vastbase": "src-e3cfef65512feb23",
|
||||
"opengauss": "src-58227ba3bc1ec894",
|
||||
"gaussdb": "src-1458564993a9d455",
|
||||
"iris": "src-1b072c57af08bec4",
|
||||
"mongodb": "src-57fdd8bfebdcd46e",
|
||||
"tdengine": "src-939715f94df1ec9c",
|
||||
|
||||
@@ -36,6 +36,7 @@ var optionalGoDrivers = map[string]struct{}{
|
||||
"highgo": {},
|
||||
"vastbase": {},
|
||||
"opengauss": {},
|
||||
"gaussdb": {},
|
||||
"iris": {},
|
||||
"mongodb": {},
|
||||
"tdengine": {},
|
||||
@@ -65,6 +66,8 @@ func normalizeRuntimeDriverType(driverType string) string {
|
||||
return "kingbase"
|
||||
case "opengauss", "open_gauss", "open-gauss":
|
||||
return "opengauss"
|
||||
case "gaussdb", "gauss_db", "gauss-db":
|
||||
return "gaussdb"
|
||||
case "intersystems", "intersystemsiris", "inter-systems-iris", "inter-systems":
|
||||
return "iris"
|
||||
case "elastic":
|
||||
@@ -116,6 +119,8 @@ func driverDisplayName(driverType string) string {
|
||||
return "Vastbase"
|
||||
case "opengauss":
|
||||
return "OpenGauss"
|
||||
case "gaussdb":
|
||||
return "GaussDB"
|
||||
case "iris":
|
||||
return "InterSystems IRIS"
|
||||
case "mongodb":
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestNewCompatibleDriversAreOptionalAgentDrivers(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
SetExternalDriverDownloadDirectory(tmpDir)
|
||||
|
||||
for _, driverType := range []string{"oceanbase", "opengauss", "open_gauss", "starrocks", "iris", "intersystems"} {
|
||||
for _, driverType := range []string{"oceanbase", "opengauss", "open_gauss", "gaussdb", "gauss_db", "starrocks", "iris", "intersystems"} {
|
||||
if IsBuiltinDriver(driverType) {
|
||||
t.Fatalf("%s 不应是免安装内置驱动", driverType)
|
||||
}
|
||||
|
||||
262
internal/db/gaussdb_impl.go
Normal file
262
internal/db/gaussdb_impl.go
Normal file
@@ -0,0 +1,262 @@
|
||||
//go:build gonavi_full_drivers || gonavi_gaussdb_driver
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
"GoNavi-Wails/internal/ssh"
|
||||
"GoNavi-Wails/internal/utils"
|
||||
|
||||
_ "github.com/HuaweiCloudDeveloper/gaussdb-go/stdlib"
|
||||
)
|
||||
|
||||
const defaultGaussDBPort = 5432
|
||||
|
||||
// GaussDB 使用独立 gaussdb:// URI 与官方 database/sql 驱动,
|
||||
// 元数据与大多数 SQL 行为按 PG-like 路径复用。
|
||||
type GaussDB struct {
|
||||
PostgresDB
|
||||
}
|
||||
|
||||
func applyGaussDBURI(config connection.ConnectionConfig) connection.ConnectionConfig {
|
||||
uriText := strings.TrimSpace(config.URI)
|
||||
if uriText == "" {
|
||||
return config
|
||||
}
|
||||
parsed, ok := parseConnectionURI(uriText, "gaussdb", "postgres", "postgresql")
|
||||
if !ok {
|
||||
return config
|
||||
}
|
||||
|
||||
if parsed.User != nil {
|
||||
if config.User == "" {
|
||||
config.User = parsed.User.Username()
|
||||
}
|
||||
if pass, ok := parsed.User.Password(); ok && config.Password == "" {
|
||||
config.Password = pass
|
||||
}
|
||||
}
|
||||
|
||||
if dbName := strings.TrimPrefix(parsed.Path, "/"); dbName != "" && config.Database == "" {
|
||||
config.Database = dbName
|
||||
}
|
||||
|
||||
defaultPort := config.Port
|
||||
if defaultPort <= 0 {
|
||||
defaultPort = defaultGaussDBPort
|
||||
}
|
||||
if strings.TrimSpace(config.Host) == "" && strings.TrimSpace(parsed.Host) != "" {
|
||||
host, port, ok := parseHostPortWithDefault(parsed.Host, defaultPort)
|
||||
if ok {
|
||||
config.Host = host
|
||||
config.Port = port
|
||||
}
|
||||
}
|
||||
if config.Port <= 0 {
|
||||
config.Port = defaultGaussDBPort
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (g *GaussDB) getDSN(config connection.ConnectionConfig) string {
|
||||
runConfig := applyGaussDBURI(config)
|
||||
dbname := runConfig.Database
|
||||
if dbname == "" {
|
||||
dbname = "postgres"
|
||||
}
|
||||
if runConfig.Port <= 0 {
|
||||
runConfig.Port = defaultGaussDBPort
|
||||
}
|
||||
if strings.TrimSpace(runConfig.Host) != "" {
|
||||
if host, port, err := net.SplitHostPort(runConfig.Host); err == nil {
|
||||
runConfig.Host = host
|
||||
if p, convErr := strconv.Atoi(port); convErr == nil && p > 0 {
|
||||
runConfig.Port = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "gaussdb",
|
||||
Host: net.JoinHostPort(runConfig.Host, strconv.Itoa(runConfig.Port)),
|
||||
Path: "/" + dbname,
|
||||
}
|
||||
u.User = url.UserPassword(runConfig.User, runConfig.Password)
|
||||
q := url.Values{}
|
||||
q.Set("sslmode", resolvePostgresSSLMode(runConfig))
|
||||
applyPostgresSSLPathParams(q, runConfig)
|
||||
q.Set("connect_timeout", strconv.Itoa(getConnectTimeoutSeconds(runConfig)))
|
||||
mergeConnectionParamsFromConfigWithAllowlist(q, runConfig, postgresConnectionParamNames, "gaussdb", "postgres", "postgresql")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (g *GaussDB) Connect(config connection.ConnectionConfig) error {
|
||||
if supported, reason := DriverRuntimeSupportStatus("gaussdb"); !supported {
|
||||
if strings.TrimSpace(reason) == "" {
|
||||
reason = "GaussDB 纯 Go 驱动未启用,请先在驱动管理中安装启用"
|
||||
}
|
||||
return fmt.Errorf("%s", reason)
|
||||
}
|
||||
|
||||
runConfig := applyGaussDBURI(config)
|
||||
g.pingTimeout = getConnectTimeout(runConfig)
|
||||
|
||||
cleanupOnFailure := true
|
||||
defer func() {
|
||||
if !cleanupOnFailure {
|
||||
return
|
||||
}
|
||||
if g.conn != nil {
|
||||
_ = g.conn.Close()
|
||||
g.conn = nil
|
||||
}
|
||||
if g.forwarder != nil {
|
||||
_ = g.forwarder.Close()
|
||||
g.forwarder = nil
|
||||
}
|
||||
}()
|
||||
|
||||
if runConfig.UseSSH {
|
||||
logger.Infof("GaussDB 使用 SSH 连接:地址=%s:%d 用户=%s", runConfig.Host, runConfig.Port, runConfig.User)
|
||||
|
||||
forwarder, err := ssh.GetOrCreateLocalForwarder(runConfig.SSH, runConfig.Host, runConfig.Port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建 SSH 隧道失败:%w", err)
|
||||
}
|
||||
g.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)
|
||||
}
|
||||
|
||||
localConfig := runConfig
|
||||
localConfig.Host = host
|
||||
localConfig.Port = port
|
||||
localConfig.UseSSH = false
|
||||
|
||||
runConfig = localConfig
|
||||
logger.Infof("GaussDB 通过本地端口转发连接:%s -> %s:%d", forwarder.LocalAddr, config.Host, config.Port)
|
||||
}
|
||||
|
||||
sslAttempts := []connection.ConnectionConfig{runConfig}
|
||||
if shouldTrySSLPreferredFallback(runConfig) {
|
||||
sslAttempts = append(sslAttempts, withSSLDisabled(runConfig))
|
||||
}
|
||||
|
||||
var failures []string
|
||||
for sslIndex, sslConfig := range sslAttempts {
|
||||
sslLabel := "SSL"
|
||||
if sslIndex > 0 {
|
||||
sslLabel = "明文回退"
|
||||
}
|
||||
|
||||
attemptDBs := resolvePostgresConnectDatabases(sslConfig)
|
||||
for _, dbName := range attemptDBs {
|
||||
attemptConfig := sslConfig
|
||||
attemptConfig.Database = dbName
|
||||
dsn := g.getDSN(attemptConfig)
|
||||
|
||||
dbConn, err := sql.Open("gaussdb", dsn)
|
||||
if err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s 数据库=%s 打开连接失败: %v", sslLabel, dbName, err))
|
||||
continue
|
||||
}
|
||||
g.conn = dbConn
|
||||
|
||||
if err := g.Ping(); err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s 数据库=%s 验证失败: %v", sslLabel, dbName, err))
|
||||
_ = dbConn.Close()
|
||||
g.conn = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if sslIndex > 0 {
|
||||
logger.Warnf("GaussDB SSL 优先连接失败,已回退至明文连接")
|
||||
}
|
||||
if strings.TrimSpace(config.Database) == "" && !strings.EqualFold(dbName, "postgres") {
|
||||
logger.Infof("GaussDB 自动选择连接数据库:%s", dbName)
|
||||
}
|
||||
|
||||
g.ensureSearchPath(dsn)
|
||||
|
||||
cleanupOnFailure = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(failures) == 0 {
|
||||
return fmt.Errorf("连接建立后验证失败:未找到可用的连接数据库")
|
||||
}
|
||||
return fmt.Errorf("连接建立后验证失败:%s", strings.Join(failures, ";"))
|
||||
}
|
||||
|
||||
func (g *GaussDB) ensureSearchPath(baseDSN string) {
|
||||
if g.conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rawSchemas := g.queryUserSchemas()
|
||||
if len(rawSchemas) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
searchPathSQL, normalizedSchemas := buildKingbaseSearchPathCommon(rawSchemas)
|
||||
if strings.TrimSpace(searchPathSQL) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
searchPathDSNVal := strings.Join(normalizedSchemas, ",")
|
||||
u, parseErr := url.Parse(baseDSN)
|
||||
if parseErr == nil {
|
||||
q := u.Query()
|
||||
q.Set("search_path", searchPathDSNVal)
|
||||
u.RawQuery = q.Encode()
|
||||
newDSN := u.String()
|
||||
|
||||
newDB, err := sql.Open("gaussdb", newDSN)
|
||||
if err == nil {
|
||||
newDB.SetConnMaxLifetime(5 * time.Minute)
|
||||
oldConn := g.conn
|
||||
g.conn = newDB
|
||||
if err := g.Ping(); err == nil {
|
||||
_ = oldConn.Close()
|
||||
logger.Infof("GaussDB 已通过 DSN 配置 search_path:%s", searchPathDSNVal)
|
||||
return
|
||||
}
|
||||
_ = newDB.Close()
|
||||
g.conn = oldConn
|
||||
logger.Warnf("GaussDB DSN search_path 验证失败,回退至 SET 方式")
|
||||
}
|
||||
}
|
||||
|
||||
timeout := g.pingTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
if _, err := g.conn.ExecContext(ctx, fmt.Sprintf("SET search_path TO %s", searchPathSQL)); err != nil {
|
||||
logger.Warnf("GaussDB 设置 search_path 失败:%v", err)
|
||||
return
|
||||
}
|
||||
logger.Infof("GaussDB 已通过 SET 设置 search_path:%s", searchPathSQL)
|
||||
}
|
||||
Reference in New Issue
Block a user