diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index de2a2c43..617fae9f 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -6361,7 +6361,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc if (plan.warning) message.warning(plan.warning); }); - // 后端附带的提示信息(如数据源不支持原生多语句执行的回退提示) + // 后端附带的提示信息(如本次改走逐条执行的多语句回退提示) if (res.message) { message.info(res.message); } diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index f3990a45..447606ba 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -1420,6 +1420,7 @@ func shouldUseNativeMultiResultBatch(dbType string, statements []string, allRead func shouldPreferPlainReadQueryResult(dbType string) bool { switch strings.ToLower(strings.TrimSpace(dbType)) { case "postgres", "postgresql", + "oracle", "kingbase", "kingbase8", "kingbasees", "kingbasev8", "highgo", "vastbase", "opengauss", "open_gauss", "open-gauss", diff --git a/internal/app/methods_db_i18n_test.go b/internal/app/methods_db_i18n_test.go index cae8c07d..eb1e08d5 100644 --- a/internal/app/methods_db_i18n_test.go +++ b/internal/app/methods_db_i18n_test.go @@ -529,7 +529,7 @@ func TestMethodsDBQueryMultiMessagesUseLocalizedText(t *testing.T) { rawMessages := []string{ `fmt.Sprintf("第 %d 条语句执行失败: %v", idx+1, err)`, `fmt.Sprintf("(前 %d 条已执行成功)", len(resultSets))`, - `fmt.Sprintf("当前数据源(%s)不支持原生多语句执行,已自动拆分为 %d 条语句逐条执行。", runConfig.Type, len(statements))`, + `fmt.Sprintf("当前数据源(%s)本次未走原生多语句结果路径,已自动拆分为 %d 条语句逐条执行。", runConfig.Type, len(statements))`, } keys := []string{ "db.backend.error.multi_statement_execution_failed", @@ -629,7 +629,7 @@ func TestMethodsDBQueryMultiSequentialFallbackUsesEnglishMessages(t *testing.T) if !result.Success { t.Fatalf("expected DBQueryMulti success, got failure: %+v", result) } - if result.Message != "The current data source (mysql) does not support native multi-statement execution. It was automatically split into 2 statements and executed sequentially." { + if result.Message != "The current data source (mysql) did not use the native multi-statement result path for this execution. It was automatically split into 2 statements and executed sequentially." { t.Fatalf("expected localized sequential fallback message, got %q", result.Message) } } diff --git a/internal/app/methods_db_multi_test.go b/internal/app/methods_db_multi_test.go index 4462b2e4..605d89f5 100644 --- a/internal/app/methods_db_multi_test.go +++ b/internal/app/methods_db_multi_test.go @@ -1616,7 +1616,7 @@ func TestDBQueryMultiRunsSQLServerStatisticsBatchNatively(t *testing.T) { if !result.Success { t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) } - if strings.Contains(result.Message, "不支持原生多语句执行") { + if strings.Contains(result.Message, "逐条执行") { t.Fatalf("expected SQL Server statistics batch to avoid sequential fallback warning, got %q", result.Message) } if fakeDB.multiCalls != 1 { @@ -1939,6 +1939,73 @@ func TestDBQueryMultiPrefersPlainQueryForDamengReadResults(t *testing.T) { } } +func TestDBQueryMultiPrefersPlainQueryForOceanBaseOracleReadResults(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + }) + + query := "SELECT * FROM EINP_BASICINFO.AC01" + nativeEmptyRowsResult := []connection.ResultSetData{{ + Rows: []map[string]interface{}{}, + Columns: []string{"AAC001", "AAC003"}, + }} + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: { + {"AAC001": 1001, "AAC003": "张三"}, + }, + }, + fieldMap: map[string][]string{ + query: {"AAC001", "AAC003"}, + }, + multiResult: map[string][]connection.ResultSetData{ + query: nativeEmptyRowsResult, + }, + queryErr: map[string]error{}, + } + fakeDB := &fakeNativeMultiResultDB{fakeBatchWriteDB: baseDB} + newDatabaseFunc = func(dbType string) (db.Database, error) { + return fakeDB, nil + } + + app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test")) + config := connection.ConnectionConfig{ + Type: "oceanbase", + Host: "127.0.0.1", + Port: 2881, + User: "SBDEVREAD", + OceanBaseProtocol: "oracle", + } + + result := app.DBQueryMulti(config, "SBDEV", query, "oceanbase-oracle-plain-query-result-test") + if !result.Success { + t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message) + } + if fakeDB.multiCalls != 0 { + t.Fatalf("expected OceanBase Oracle read query to skip top-level native multi-result path, got %d calls", fakeDB.multiCalls) + } + if baseDB.session == nil { + t.Fatal("expected DBQueryMulti to open a pinned session for OceanBase Oracle read query") + } + if baseDB.session.queryCalls != 1 { + t.Fatalf("expected OceanBase Oracle read query to use plain session query once, got %d calls", baseDB.session.queryCalls) + } + resultSets, ok := result.Data.([]connection.ResultSetData) + if !ok { + t.Fatalf("expected []connection.ResultSetData, got %T", result.Data) + } + if len(resultSets) != 1 { + t.Fatalf("expected one result set, got %#v", resultSets) + } + if !reflect.DeepEqual(resultSets[0].Columns, []string{"AAC001", "AAC003"}) { + t.Fatalf("expected plain query columns, got %#v", resultSets[0].Columns) + } + if got := resultSets[0].Rows[0]["AAC003"]; got != "张三" { + t.Fatalf("expected plain query SELECT result AAC003=张三, got %#v", got) + } +} + func TestDBQueryMultiUsesPinnedSessionForSequentialFallback(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc t.Cleanup(func() { @@ -2371,3 +2438,48 @@ func TestExecuteManagedSQLTransactionStatementsPrefersPlainQueryForDamengReadRes t.Fatalf("expected plain query SELECT result NAME=timer_a, got %#v", got) } } + +func TestExecuteManagedSQLTransactionStatementsPrefersPlainQueryForOceanBaseOracleReadResults(t *testing.T) { + query := "SELECT * FROM EINP_BASICINFO.AC01" + baseDB := &fakeBatchWriteDB{ + queryMap: map[string][]map[string]interface{}{ + query: { + {"AAC001": 1001, "AAC003": "张三"}, + }, + }, + fieldMap: map[string][]string{ + query: {"AAC001", "AAC003"}, + }, + multiResult: map[string][]connection.ResultSetData{ + query: {{ + Rows: []map[string]interface{}{}, + Columns: []string{"AAC001", "AAC003"}, + }}, + }, + queryErr: map[string]error{}, + } + session := &fakeBatchWriteSession{parent: baseDB} + + results, err := executeManagedSQLTransactionStatements( + context.Background(), + session, + connection.ConnectionConfig{Type: "oceanbase", OceanBaseProtocol: "oracle"}, + []string{query}, + nil, + ) + if err != nil { + t.Fatalf("expected executeManagedSQLTransactionStatements success, got %v", err) + } + if session.queryCalls != 1 { + t.Fatalf("expected OceanBase Oracle managed read query to use plain query once, got %d calls", session.queryCalls) + } + if len(results) != 1 { + t.Fatalf("expected one result set, got %#v", results) + } + if !reflect.DeepEqual(results[0].Columns, []string{"AAC001", "AAC003"}) { + t.Fatalf("expected plain query columns, got %#v", results[0].Columns) + } + if got := results[0].Rows[0]["AAC003"]; got != "张三" { + t.Fatalf("expected plain query SELECT result AAC003=张三, got %#v", got) + } +} diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 09532d0b..84862b99 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "{{count}} Mitglieder gefunden", "db.backend.message.mongo_primary_credentials_label": "Primär-Anmeldedaten", "db.backend.message.mongo_replica_credentials_label": "Replikat-Anmeldedaten", - "db.backend.message.multi_statement_sequential_fallback": "Die aktuelle Datenquelle ({{dbType}}) unterstützt keine native Ausführung mehrerer Anweisungen. Sie wurde automatisch in {{count}} Anweisungen aufgeteilt und nacheinander ausgeführt.", + "db.backend.message.multi_statement_sequential_fallback": "Bei dieser Ausführung hat die aktuelle Datenquelle ({{dbType}}) nicht den nativen Mehrfach-Statement-Ergebnispfad verwendet. Sie wurde automatisch in {{count}} Anweisungen aufgeteilt und nacheinander ausgeführt.", "db.backend.message.procedure_dropped": "Gespeicherte Prozedur gelöscht", "db.backend.message.release_success": "Verbindung freigegeben", "db.backend.message.schema_created": "Schema erstellt", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 266c3df7..b4c8d0b0 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "Discovered {{count}} members", "db.backend.message.mongo_primary_credentials_label": "primary credentials", "db.backend.message.mongo_replica_credentials_label": "replica credentials", - "db.backend.message.multi_statement_sequential_fallback": "The current data source ({{dbType}}) does not support native multi-statement execution. It was automatically split into {{count}} statements and executed sequentially.", + "db.backend.message.multi_statement_sequential_fallback": "The current data source ({{dbType}}) did not use the native multi-statement result path for this execution. It was automatically split into {{count}} statements and executed sequentially.", "db.backend.message.procedure_dropped": "Stored procedure dropped", "db.backend.message.release_success": "Connection released", "db.backend.message.schema_created": "Schema created", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index ddbbdbb4..75f8da61 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "{{count}} 件のメンバーを検出しました", "db.backend.message.mongo_primary_credentials_label": "プライマリ認証情報", "db.backend.message.mongo_replica_credentials_label": "レプリカ認証情報", - "db.backend.message.multi_statement_sequential_fallback": "現在のデータソース({{dbType}})はネイティブな複数ステートメント実行をサポートしていないため、{{count}} 件のステートメントに自動分割して順番に実行しました。", + "db.backend.message.multi_statement_sequential_fallback": "現在のデータソース({{dbType}})では今回の実行でネイティブな複数ステートメント結果パスを使用せず、{{count}} 件のステートメントに自動分割して順番に実行しました。", "db.backend.message.procedure_dropped": "ストアドプロシージャを削除しました", "db.backend.message.release_success": "接続を解放しました", "db.backend.message.schema_created": "スキーマを作成しました", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index fd1fd5d6..38619ab5 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "Обнаружено участников: {{count}}", "db.backend.message.mongo_primary_credentials_label": "учетные данные primary", "db.backend.message.mongo_replica_credentials_label": "учетные данные реплики", - "db.backend.message.multi_statement_sequential_fallback": "Текущий источник данных ({{dbType}}) не поддерживает нативное выполнение нескольких операторов. Запрос был автоматически разделен на {{count}} операторов и выполнен последовательно.", + "db.backend.message.multi_statement_sequential_fallback": "Для этого выполнения текущий источник данных ({{dbType}}) не использовал нативный путь результатов для нескольких операторов. Запрос был автоматически разделен на {{count}} операторов и выполнен последовательно.", "db.backend.message.procedure_dropped": "Хранимая процедура удалена", "db.backend.message.release_success": "Подключение освобождено", "db.backend.message.schema_created": "Схема создана", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 871c5ec6..3cd535f4 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "发现 {{count}} 个成员", "db.backend.message.mongo_primary_credentials_label": "主库凭据", "db.backend.message.mongo_replica_credentials_label": "从库凭据", - "db.backend.message.multi_statement_sequential_fallback": "当前数据源({{dbType}})不支持原生多语句执行,已自动拆分为 {{count}} 条语句逐条执行。", + "db.backend.message.multi_statement_sequential_fallback": "当前数据源({{dbType}})本次未走原生多语句结果路径,已自动拆分为 {{count}} 条语句逐条执行。", "db.backend.message.procedure_dropped": "存储过程删除成功", "db.backend.message.release_success": "连接已释放", "db.backend.message.schema_created": "模式创建成功", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 3e226eb3..7764f69e 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -4739,7 +4739,7 @@ "db.backend.message.mongo_members_discovered": "發現 {{count}} 個成員", "db.backend.message.mongo_primary_credentials_label": "主庫憑證", "db.backend.message.mongo_replica_credentials_label": "從庫憑證", - "db.backend.message.multi_statement_sequential_fallback": "目前資料來源({{dbType}})不支援原生多語句執行,已自動拆分為 {{count}} 條語句逐條執行。", + "db.backend.message.multi_statement_sequential_fallback": "目前資料來源({{dbType}})本次未走原生多語句結果路徑,已自動拆分為 {{count}} 條語句逐條執行。", "db.backend.message.procedure_dropped": "預存程序刪除成功", "db.backend.message.release_success": "連線已釋放", "db.backend.message.schema_created": "模式建立成功",