Compare commits

..

8 Commits

4 changed files with 163 additions and 1 deletions

View File

@@ -37,6 +37,13 @@ describe('formatSqlExecutionError', () => {
expect(formatted).toContain('Raw error: driver returned unexpected status 123');
});
it('recognizes driver bad connection during SQL execution as timeout semantics', () => {
const formatted = formatSqlExecutionError('第 1 条语句执行失败driver: bad connection');
expect(formatted).toContain('Semantic meaning: Query timed out or was canceled');
expect(formatted).toContain('Raw error: 第 1 条语句执行失败driver: bad connection');
});
it('recognizes localized connection-timeout wrappers as timeout semantics', () => {
const translate = (key: string, params?: Record<string, unknown>) => {
if (key === 'query_editor.sql_error.wrapper.semantic_line') {

View File

@@ -135,6 +135,7 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
/context cancelled/i,
/timeout/i,
/timed out/i,
/driver:\s*bad connection/i,
],
},
{

View File

@@ -1,6 +1,7 @@
package app
import (
"net/url"
"strconv"
"strings"
@@ -8,6 +9,8 @@ import (
"GoNavi-Wails/internal/db"
)
const defaultOceanBaseOracleQueryTimeoutSeconds = 120
func normalizeRunConfig(config connection.ConnectionConfig, dbName string) connection.ConnectionConfig {
runConfig := config
name := strings.TrimSpace(dbName)
@@ -23,7 +26,9 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
case "kafka", "apache-kafka", "apache_kafka":
// Kafka 的 Database 字段表示默认 Topic不能被树上的 synthetic database(topics) 覆盖。
case "oceanbase":
if !isOceanBaseOracleProtocol(config) {
if isOceanBaseOracleProtocol(config) {
runConfig = applyOceanBaseOracleCurrentSchemaInit(runConfig, name)
} else {
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", "trino", "rabbitmq", "rabbit-mq", "rabbit_mq":
@@ -46,6 +51,63 @@ func normalizeRunConfig(config connection.ConnectionConfig, dbName string) conne
return runConfig
}
func applyOceanBaseOracleCurrentSchemaInit(config connection.ConnectionConfig, schema string) connection.ConnectionConfig {
normalizedSchema := strings.TrimSpace(schema)
if normalizedSchema == "" {
return config
}
if config.Timeout <= 0 {
// OceanBase Oracle 查询经常需要经过 OBProxy/Oracle driver 的读等待;默认 30s 容易把慢查询误报成 driver: bad connection。
config.Timeout = defaultOceanBaseOracleQueryTimeoutSeconds
}
values, err := url.ParseQuery(strings.TrimSpace(config.ConnectionParams))
if err != nil {
return config
}
statement := "ALTER SESSION SET CURRENT_SCHEMA = " + quoteOracleCurrentSchemaIdentifier(normalizedSchema)
for _, existing := range values["init"] {
if strings.EqualFold(strings.TrimSpace(existing), statement) {
return config
}
}
values.Add("init", statement)
config.ConnectionParams = values.Encode()
return config
}
func quoteOracleCurrentSchemaIdentifier(schema string) string {
normalized := strings.TrimSpace(schema)
if normalized == "" {
return normalized
}
if isSimpleOracleIdentifier(normalized) {
return strings.ToUpper(normalized)
}
return `"` + strings.ReplaceAll(normalized, `"`, `""`) + `"`
}
func isSimpleOracleIdentifier(value string) bool {
text := strings.TrimSpace(value)
if text == "" {
return false
}
for index, r := range text {
isLetter := (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')
isDigit := r >= '0' && r <= '9'
isSpecial := r == '_' || r == '$' || r == '#'
if index == 0 {
if !isLetter && r != '_' {
return false
}
continue
}
if !isLetter && !isDigit && !isSpecial {
return false
}
}
return true
}
func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string, tableName string) (string, string) {
rawTable := strings.TrimSpace(tableName)
rawDB := strings.TrimSpace(dbName)

View File

@@ -0,0 +1,92 @@
package app
import (
"net/url"
"testing"
"GoNavi-Wails/internal/connection"
)
func TestNormalizeRunConfig_OceanBaseOracleAddsCurrentSchemaInit(t *testing.T) {
t.Parallel()
config := connection.ConnectionConfig{
Type: "oceanbase",
Database: "OBORCL",
OceanBaseProtocol: "oracle",
}
runConfig := normalizeRunConfig(config, "sbdev")
if runConfig.Database != "OBORCL" {
t.Fatalf("expected OceanBase Oracle service name to stay OBORCL, got %q", runConfig.Database)
}
if runConfig.Timeout != defaultOceanBaseOracleQueryTimeoutSeconds {
t.Fatalf("expected OceanBase Oracle default query timeout %d, got %d", defaultOceanBaseOracleQueryTimeoutSeconds, runConfig.Timeout)
}
values, err := url.ParseQuery(runConfig.ConnectionParams)
if err != nil {
t.Fatalf("unexpected connection params parse error: %v", err)
}
initValues := values["init"]
if len(initValues) != 1 || initValues[0] != "ALTER SESSION SET CURRENT_SCHEMA = SBDEV" {
t.Fatalf("expected current schema init for selected schema, got %#v", initValues)
}
}
func TestNormalizeRunConfig_OceanBaseOraclePreservesExplicitTimeoutAndExistingInit(t *testing.T) {
t.Parallel()
config := connection.ConnectionConfig{
Type: "oceanbase",
Database: "OBORCL",
OceanBaseProtocol: "oracle",
Timeout: 45,
ConnectionParams: "init=ALTER+SESSION+SET+NLS_DATE_FORMAT%3D%27YYYY-MM-DD%27&timeout=10s",
}
runConfig := normalizeRunConfig(config, "sbdev")
if runConfig.Timeout != 45 {
t.Fatalf("expected explicit timeout to be preserved, got %d", runConfig.Timeout)
}
values, err := url.ParseQuery(runConfig.ConnectionParams)
if err != nil {
t.Fatalf("unexpected connection params parse error: %v", err)
}
initValues := values["init"]
if len(initValues) != 2 {
t.Fatalf("expected existing init plus current schema init, got %#v", initValues)
}
if initValues[0] != "ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD'" {
t.Fatalf("expected existing init to stay first, got %#v", initValues)
}
if initValues[1] != "ALTER SESSION SET CURRENT_SCHEMA = SBDEV" {
t.Fatalf("expected current schema init appended, got %#v", initValues)
}
if values.Get("timeout") != "10s" {
t.Fatalf("expected non-init connection param preserved, got %q", values.Get("timeout"))
}
}
func TestQuoteOracleCurrentSchemaIdentifier(t *testing.T) {
t.Parallel()
cases := []struct {
name string
raw string
want string
}{
{name: "lower simple", raw: "sbdev", want: "SBDEV"},
{name: "normal uppercase", raw: "SBDEV", want: "SBDEV"},
{name: "quoted mixed case required", raw: "Sb Dev", want: `"Sb Dev"`},
{name: "quote escaping", raw: `A"B`, want: `"A""B"`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := quoteOracleCurrentSchemaIdentifier(tc.raw); got != tc.want {
t.Fatalf("expected %q, got %q", tc.want, got)
}
})
}
}