mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 17:44:14 +08:00
✨ feat(oracle): 支持 SID 连接模式
- 在连接弹窗中切换 Service Name 与 SID,并完善 URI 和参数回显 - 使用 SID 查询参数生成 Oracle DSN,兼容 Navicat 导入及历史配置 - 补充多语言、弹窗滚动布局和前后端回归测试
This commit is contained in:
@@ -45,7 +45,7 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
|
||||
runConfig = runConfig.WithRuntimeDatabaseOverride(name)
|
||||
}
|
||||
default:
|
||||
// oracle: dbName 表示 schema/owner,不能覆盖 config.Database(服务名)
|
||||
// oracle: dbName 表示 schema/owner,不能覆盖 config.Database(服务名)或 SID(SID 模式)
|
||||
// sqlite: 无需设置 Database
|
||||
// 其他 custom: 语义不明确,避免污染缓存 key
|
||||
}
|
||||
|
||||
@@ -146,7 +146,10 @@ func parseNavicatNCXConnectionWithText(item navicatNCXConnection, text navicatTe
|
||||
config.Database = strings.TrimSpace(item.TNS)
|
||||
}
|
||||
if configType == "oracle" && strings.EqualFold(strings.TrimSpace(item.OraServiceNameType), "SID") && strings.TrimSpace(config.Database) != "" {
|
||||
// SID 模式:SID 值仅存于 ConnectionParams(go-ora 的 SID 查询参数),
|
||||
// Database(服务名)置空避免 DSN path 冗余,语义与内置连接表单一致。
|
||||
config.ConnectionParams = "SID=" + strings.TrimSpace(config.Database)
|
||||
config.Database = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,9 @@ func TestImportConnectionsPayloadNavicatNCXMapsOracleSIDAndRedisDB(t *testing.T)
|
||||
if oracleConn.Config.Type != "oracle" || oracleConn.Config.ConnectionParams != "SID=ORCL" {
|
||||
t.Fatalf("expected oracle SID connection params, got %#v", oracleConn.Config)
|
||||
}
|
||||
if oracleConn.Config.Database != "" {
|
||||
t.Fatalf("expected oracle SID import to leave Database empty (SID only in ConnectionParams), got %q", oracleConn.Config.Database)
|
||||
}
|
||||
resolvedOracle, err := app.resolveConnectionSecrets(oracleConn.Config)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveConnectionSecrets for oracle returned error: %v", err)
|
||||
|
||||
@@ -263,6 +263,79 @@ func TestOracleDSN_EscapesUserAndPassword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleDSN_SIDModeOmitsPathAndCarriesSIDParam(t *testing.T) {
|
||||
o := &OracleDB{}
|
||||
cfg := connection.ConnectionConfig{
|
||||
Type: "oracle",
|
||||
Host: "127.0.0.1",
|
||||
Port: 1521,
|
||||
User: "system",
|
||||
Password: "secret",
|
||||
ConnectionParams: "SID=ORCL",
|
||||
}
|
||||
|
||||
if !isOracleSIDMode(cfg) {
|
||||
t.Fatal("expected isOracleSIDMode to be true when SID param present")
|
||||
}
|
||||
if got := oracleConnectionSID(cfg); got != "ORCL" {
|
||||
t.Fatalf("oracleConnectionSID = %q, want ORCL", got)
|
||||
}
|
||||
|
||||
dsn := o.getDSN(cfg)
|
||||
parsed, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("parse dsn failed: %v", err)
|
||||
}
|
||||
if strings.Trim(parsed.EscapedPath(), "/") != "" {
|
||||
t.Fatalf("SID 模式不应在 URL path 携带服务名,path=%q dsn=%s", parsed.EscapedPath(), dsn)
|
||||
}
|
||||
if got := parsed.Query().Get("SID"); got != "ORCL" {
|
||||
t.Fatalf("SID 参数 = %q, want ORCL(dsn=%s)", got, dsn)
|
||||
}
|
||||
if parsed.Query().Get("PREFETCH_ROWS") == "" {
|
||||
t.Fatalf("SID 模式应保留默认驱动参数,dsn=%s", dsn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleDSN_SIDModeOverridesLegacyDatabaseField(t *testing.T) {
|
||||
o := &OracleDB{}
|
||||
// 兼容历史数据:Navicat 导入的旧 SID 连接同时存在 Database 与 SID 参数,
|
||||
// SID 优先且 path 不携带服务名,避免冗余与日志误读。
|
||||
cfg := connection.ConnectionConfig{
|
||||
Type: "oracle",
|
||||
Host: "db.example.com",
|
||||
Port: 1521,
|
||||
User: "system",
|
||||
Password: "secret",
|
||||
Database: "ORCL",
|
||||
ConnectionParams: "SID=ORCL",
|
||||
}
|
||||
|
||||
dsn := o.getDSN(cfg)
|
||||
parsed, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("parse dsn failed: %v", err)
|
||||
}
|
||||
if strings.Trim(parsed.EscapedPath(), "/") != "" {
|
||||
t.Fatalf("SID 模式不应在 URL path 携带 Database,path=%q dsn=%s", parsed.EscapedPath(), dsn)
|
||||
}
|
||||
if got := parsed.Query().Get("SID"); got != "ORCL" {
|
||||
t.Fatalf("SID 参数 = %q, want ORCL(dsn=%s)", got, dsn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleSIDParamParsingIsCaseInsensitive(t *testing.T) {
|
||||
for _, raw := range []string{"sid=ORCL", "Sid=ORCL", "SID=ORCL", "SID =ORCL"} {
|
||||
cfg := connection.ConnectionConfig{
|
||||
Type: "oracle",
|
||||
ConnectionParams: raw,
|
||||
}
|
||||
if got := oracleConnectionSID(cfg); got != "ORCL" {
|
||||
t.Fatalf("oracleConnectionSID(%q) = %q, want ORCL", raw, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDamengDSN_KeepsRawPasswordForDriverParser(t *testing.T) {
|
||||
d := &DamengDB{}
|
||||
cfg := connection.ConnectionConfig{
|
||||
|
||||
@@ -111,13 +111,27 @@ func TestOracleDSNLogSummaryDoesNotExposePassword(t *testing.T) {
|
||||
if strings.Contains(got, "top-secret") || strings.Contains(got, "sys@tenant") {
|
||||
t.Fatalf("summary should not expose credentials, got %q", got)
|
||||
}
|
||||
for _, want := range []string{"服务名=ORCLPDB1", "DBA_PRIVILEGE=SYSDBA", "AUTH_TYPE=NORMAL"} {
|
||||
for _, want := range []string{"连接模式=服务名", "服务名=ORCLPDB1", "DBA_PRIVILEGE=SYSDBA", "AUTH_TYPE=NORMAL"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected summary to contain %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleDSNLogSummaryUsesEffectiveSIDOverLegacyDatabase(t *testing.T) {
|
||||
dsn := "oracle://sys:top-secret@127.0.0.1:1521?SID=ORCL&DBA+PRIVILEGE=SYSDBA"
|
||||
got := oracleDSNLogSummary(connection.ConnectionConfig{Database: "OLD_SERVICE"}, dsn)
|
||||
|
||||
for _, want := range []string{"连接模式=SID", "SID=ORCL", "DBA_PRIVILEGE=SYSDBA"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected summary to contain %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "OLD_SERVICE") || strings.Contains(got, "top-secret") {
|
||||
t.Fatalf("summary should use the effective SID without exposing stale or secret values, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotateOracleValidationErrorAddsClosedConnectionHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -41,17 +41,36 @@ func oracleRuntimeError(key string, params map[string]any) error {
|
||||
return fmt.Errorf("%s", localizedDriverRuntimeText(key, params))
|
||||
}
|
||||
|
||||
// oracleConnectionSID 解析连接配置(ConnectionParams / URI)中的 SID 参数。
|
||||
// SID 与 Service Name 是 Oracle 两种互斥的连接定位方式:go-ora 驱动在
|
||||
// CONNECT_DATA 中优先使用 SID(configurations/connect_config.go),因此
|
||||
// SID 模式只需把 SID 值放入 DSN 查询参数,Database(服务名)可留空。
|
||||
func oracleConnectionSID(config connection.ConnectionConfig) string {
|
||||
values := url.Values{}
|
||||
mergeConnectionParamsFromConfigWithAllowlist(values, config, oracleConnectionParamNames, "oracle")
|
||||
return oracleQueryValue(values, "SID")
|
||||
}
|
||||
|
||||
// isOracleSIDMode 报告连接是否以 SID 模式连接(存在 SID 参数时优先于服务名)。
|
||||
func isOracleSIDMode(config connection.ConnectionConfig) bool {
|
||||
return oracleConnectionSID(config) != ""
|
||||
}
|
||||
|
||||
func (o *OracleDB) getDSN(config connection.ConnectionConfig) string {
|
||||
// oracle://user:pass@host:port/service_name
|
||||
// 服务名模式:oracle://user:pass@host:port/service_name
|
||||
// SID 模式:oracle://user:pass@host:port/?SID=sid(go-ora 驱动据此组装 (SID=...))
|
||||
database := strings.TrimSpace(config.Database)
|
||||
sid := oracleConnectionSID(config)
|
||||
|
||||
u := &url.URL{
|
||||
Scheme: "oracle",
|
||||
Host: net.JoinHostPort(config.Host, strconv.Itoa(config.Port)),
|
||||
Path: "/" + database,
|
||||
}
|
||||
if sid == "" {
|
||||
u.Path = "/" + database
|
||||
u.RawPath = "/" + url.PathEscape(database)
|
||||
}
|
||||
u.User = url.UserPassword(config.User, config.Password)
|
||||
u.RawPath = "/" + url.PathEscape(database)
|
||||
q := url.Values{}
|
||||
switch normalizedSSLMode(config) {
|
||||
case sslModeRequired:
|
||||
@@ -97,18 +116,28 @@ func oracleDSNLogSummary(config connection.ConnectionConfig, dsn string) string
|
||||
}
|
||||
params = parsed.Query()
|
||||
}
|
||||
if serviceName == "" {
|
||||
serviceName = "(未配置)"
|
||||
sid := oracleQueryValue(params, "SID")
|
||||
mode := "服务名"
|
||||
targetLabel := "服务名"
|
||||
targetValue := serviceName
|
||||
if sid != "" {
|
||||
mode = "SID"
|
||||
targetLabel = "SID"
|
||||
targetValue = sid
|
||||
}
|
||||
return fmt.Sprintf("服务名=%s CONNECT_TIMEOUT=%s READ_TIMEOUT=%s SSL=%s SSL_VERIFY=%s AUTH_TYPE=%s DBA_PRIVILEGE=%s SID=%s",
|
||||
serviceName,
|
||||
if targetValue == "" {
|
||||
targetValue = "(未配置)"
|
||||
}
|
||||
return fmt.Sprintf("连接模式=%s %s=%s CONNECT_TIMEOUT=%s READ_TIMEOUT=%s SSL=%s SSL_VERIFY=%s AUTH_TYPE=%s DBA_PRIVILEGE=%s",
|
||||
mode,
|
||||
targetLabel,
|
||||
targetValue,
|
||||
oracleQueryValueOrDefault(params, "CONNECT TIMEOUT"),
|
||||
oracleQueryValueOrDefault(params, "READ TIMEOUT"),
|
||||
oracleQueryValueOrDefault(params, "SSL"),
|
||||
oracleQueryValueOrDefault(params, "SSL VERIFY"),
|
||||
oracleQueryValueOrDefault(params, "AUTH TYPE"),
|
||||
oracleQueryValueOrDefault(params, "DBA PRIVILEGE"),
|
||||
oracleQueryValueOrDefault(params, "SID"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +149,7 @@ func annotateOracleValidationError(err error) error {
|
||||
if !strings.Contains(message, "use of closed network connection") {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w(Oracle 连接在验证阶段被服务端关闭或被驱动超时中断;请检查监听端口是否为 Oracle 协议端口、Service Name 是否正确、认证参数如 DBA_PRIVILEGE/AUTH_TYPE 是否匹配)", err)
|
||||
return fmt.Errorf("%w(Oracle 连接在验证阶段被服务端关闭或被驱动超时中断;请检查监听端口是否为 Oracle 协议端口、服务名(Service Name)或 SID 是否正确、认证参数如 DBA_PRIVILEGE/AUTH_TYPE 是否匹配)", err)
|
||||
}
|
||||
|
||||
func (o *OracleDB) Connect(config connection.ConnectionConfig) (err error) {
|
||||
@@ -133,8 +162,9 @@ func (o *OracleDB) Connect(config connection.ConnectionConfig) (err error) {
|
||||
|
||||
runConfig := config
|
||||
serviceName := strings.TrimSpace(config.Database)
|
||||
if serviceName == "" {
|
||||
return fmt.Errorf("Oracle 连接缺少服务名(Service Name),请在连接配置中填写,例如 ORCLPDB1")
|
||||
sid := oracleConnectionSID(config)
|
||||
if serviceName == "" && sid == "" {
|
||||
return fmt.Errorf("Oracle 连接缺少服务名(Service Name)或 SID,请在连接配置中填写,例如 ORCLPDB1(服务名)或 ORCL(SID)")
|
||||
}
|
||||
|
||||
if config.UseSSH {
|
||||
|
||||
Reference in New Issue
Block a user