fix(query): add timeout settings entry to error page

This commit is contained in:
zipg
2026-08-15 21:53:17 +08:00
committed by GitHub
parent 8f3351f720
commit 1d7fa00fe3
13 changed files with 113 additions and 2 deletions
@@ -3,7 +3,7 @@ import { Bot, Wrench } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import type { BackendError } from "@/lib/backend/errorUtils";
import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
import { isConnectionTimeoutErrorMessage, isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
const props = defineProps<{
errorMessage: string;
@@ -12,6 +12,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
changeConnectionTimeout: [];
changeQueryTimeout: [];
fixWithAi: [errorMessage: string];
}>();
@@ -20,6 +21,10 @@ const { t } = useI18n();
</script>
<template>
<Button v-if="connectionId && isConnectionTimeoutErrorMessage(errorMessage, backendError)" variant="outline" size="sm" class="h-7 gap-1.5 px-2.5 text-xs" @click="emit('changeConnectionTimeout')">
<Wrench class="h-3.5 w-3.5" />
{{ t("editor.changeConnectionTimeout") }}
</Button>
<Button v-if="connectionId && isQueryTimeoutErrorMessage(errorMessage, backendError)" variant="outline" size="sm" class="h-7 gap-1.5 px-2.5 text-xs" @click="emit('changeQueryTimeout')">
<Wrench class="h-3.5 w-3.5" />
{{ t("editor.changeQueryTimeout") }}
@@ -0,0 +1,44 @@
// @vitest-environment happy-dom
import { createApp, h, nextTick, type App } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import QueryErrorActions from "@/components/common/QueryErrorActions.vue";
import i18n from "@/i18n";
const mountedApps: App[] = [];
async function mountActions(errorMessage: string, onChangeConnectionTimeout = vi.fn()) {
const host = document.createElement("div");
document.body.appendChild(host);
const app = createApp({
setup: () => () => h(QueryErrorActions, { errorMessage, connectionId: "postgres-1", onChangeConnectionTimeout }),
});
mountedApps.push(app);
app.use(i18n);
app.mount(host);
await nextTick();
return { host, onChangeConnectionTimeout };
}
afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.replaceChildren();
});
describe("QueryErrorActions", () => {
it("offers connection timeout settings for PostgreSQL connection creation timeouts", async () => {
const { host, onChangeConnectionTimeout } = await mountActions("PostgreSQL connection failed: Timeout occurred while creating a new object SQL text omitted from user-facing error");
const timeoutButton = Array.from(host.querySelectorAll("button")).find((button) => button.textContent?.includes("Change connection timeout"));
expect(timeoutButton).toBeTruthy();
timeoutButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
expect(onChangeConnectionTimeout).toHaveBeenCalledOnce();
});
it("does not offer connection timeout settings for query timeouts", async () => {
const { host } = await mountActions("Query timed out after 30 seconds");
expect(host.textContent).not.toContain("Change connection timeout");
expect(host.textContent).toContain("Change query timeout");
});
});
@@ -1640,6 +1640,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:error-message="String(errorMessage)"
:backend-error="activeTab.result.error"
:connection-id="activeResultConnectionId"
@change-connection-timeout="activeResultConnectionId && emit('openConnectionSettings', activeResultConnectionId, 'advanced')"
@change-query-timeout="activeResultConnectionId && emit('openConnectionSettings', activeResultConnectionId, 'advanced')"
@fix-with-ai="(message) => emit('fixWithAi', message)"
/>
@@ -1974,6 +1975,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:error-message="String(errorMessage)"
:backend-error="activeTab.result.error"
:connection-id="activeResultConnectionId"
@change-connection-timeout="activeResultConnectionId && emit('openConnectionSettings', activeResultConnectionId, 'advanced')"
@change-query-timeout="activeResultConnectionId && emit('openConnectionSettings', activeResultConnectionId, 'advanced')"
@fix-with-ai="(message) => emit('fixWithAi', message)"
/>
+1
View File
@@ -1146,6 +1146,7 @@ export default {
pressToExecute: "Press {mod}+Enter to execute",
pressToSaveSql: "Press {mod}+S to save SQL",
queryTimeoutError: "Query timed out ({seconds}s). Check whether the database connection is healthy.",
changeConnectionTimeout: "Change connection timeout",
changeQueryTimeout: "Change query timeout",
connectionMayBeLost: "The connection may have been lost. Refresh data and try again.",
showResultsPane: "Show results",
+1
View File
@@ -1124,6 +1124,7 @@ export default withEnglishFallback({
pressToExecute: "Presiona {mod}+Enter para ejecutar",
pressToSaveSql: "Presiona {mod}+S para guardar el SQL",
queryTimeoutError: "La consulta agotó el tiempo ({seconds}s). Comprueba la conexión a la base de datos.",
changeConnectionTimeout: "Cambiar tiempo de espera de conexión",
changeQueryTimeout: "Cambiar tiempo de espera de consulta",
connectionMayBeLost: "Es posible que la conexión se haya perdido. Actualiza los datos e inténtalo de nuevo.",
showResultsPane: "Mostrar resultados",
+1
View File
@@ -1122,6 +1122,7 @@ export default withEnglishFallback({
pressToExecute: "Premi {mod}+Enter per eseguire",
pressToSaveSql: "Premi {mod}+S per salvare SQL",
queryTimeoutError: "Timeout della query ({seconds}s). Verifica se la connessione al database è integra.",
changeConnectionTimeout: "Modifica timeout connessione",
changeQueryTimeout: "Modifica timeout query",
connectionMayBeLost: "La connessione potrebbe essere andata perduta. Aggiorna i dati e riprova.",
showResultsPane: "Mostra risultati",
+1
View File
@@ -1143,6 +1143,7 @@ export default withEnglishFallback({
pressToExecute: "{mod}+Enter で実行",
pressToSaveSql: "{mod}+S でSQLを保存",
queryTimeoutError: "クエリがタイムアウトしました({seconds}秒)。データベース接続が正常か確認してください。",
changeConnectionTimeout: "接続タイムアウトを変更",
changeQueryTimeout: "クエリタイムアウトを変更",
connectionMayBeLost: "接続が失われた可能性があります。データを更新して再試行してください。",
showResultsPane: "結果を表示",
+1
View File
@@ -1038,6 +1038,7 @@ export default withEnglishFallback({
pressToExecute: "{mod}+Enter를 눌러 실행",
pressToSaveSql: "{mod}+S를 눌러 SQL 저장",
queryTimeoutError: "쿼리 시간 초과 ({seconds}초). 데이터베이스 연결이 정상인지 확인하세요.",
changeConnectionTimeout: "연결 제한 시간 변경",
changeQueryTimeout: "쿼리 제한 시간 변경",
connectionMayBeLost: "연결이 끊어졌을 수 있습니다. 데이터를 새로고침하고 다시 시도하세요.",
showResultsPane: "결과 표시",
+1
View File
@@ -1123,6 +1123,7 @@ export default withEnglishFallback({
pressToExecute: "Pressione {mod}+Enter para executar",
pressToSaveSql: "Pressione {mod}+S para salvar o SQL",
queryTimeoutError: "A consulta expirou ({seconds}s). Verifique se a conexão com o banco de dados está saudável.",
changeConnectionTimeout: "Alterar tempo limite da conexão",
changeQueryTimeout: "Alterar tempo limite da consulta",
connectionMayBeLost: "A conexão pode ter sido perdida. Atualize os dados e tente novamente.",
showResultsPane: "Mostrar resultados",
+1
View File
@@ -1070,6 +1070,7 @@ export default withEnglishFallback({
pressToExecute: "按 {mod}+Enter 执行查询",
pressToSaveSql: "按 {mod}+S 保存 SQL",
queryTimeoutError: "查询超时 ({seconds}s),请检查数据库连接是否正常",
changeConnectionTimeout: "修改超时时间",
changeQueryTimeout: "修改查询超时时间",
connectionMayBeLost: "连接可能已断开,请刷新数据重试",
showResultsPane: "显示结果",
+1
View File
@@ -1122,6 +1122,7 @@ export default withEnglishFallback({
pressToExecute: "按 {mod}+Enter 執行查詢",
pressToSaveSql: "按 {mod}+S 儲存 SQL",
queryTimeoutError: "查詢逾時 ({seconds}s),請檢查資料庫連線是否正常",
changeConnectionTimeout: "修改連線逾時時間",
changeQueryTimeout: "修改查詢逾時時間",
connectionMayBeLost: "連線可能已斷開,請重新整理資料重試",
showResultsPane: "顯示結果",
+39 -1
View File
@@ -1,5 +1,43 @@
import { describe, expect, it } from "vitest";
import { isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
import { isConnectionTimeoutErrorMessage, isQueryTimeoutErrorMessage } from "@/lib/sql/queryError";
describe("isConnectionTimeoutErrorMessage", () => {
it("detects connection creation and handshake timeouts", () => {
expect(isConnectionTimeoutErrorMessage("PostgreSQL connection failed: Timeout occurred while creating a new object")).toBe(true);
expect(isConnectionTimeoutErrorMessage("MySQL connection failed: TLS handshake timed out after 10 seconds")).toBe(true);
expect(isConnectionTimeoutErrorMessage("The connection attempt timed out")).toBe(true);
});
it("detects structured connect-stage timeouts", () => {
const timeoutError = {
version: 1,
code: "DBX-JDBC-2001",
messageKey: "backendErrors.jdbc.operationTimedOut",
messageParams: { stage: "connect" },
source: "jdbcAgent",
operationOutcome: "not_started" as const,
} as const;
expect(isConnectionTimeoutErrorMessage("Database operation timed out (stage: connect).", timeoutError)).toBe(true);
});
it("does not classify query, pool checkout, cancellation, or syntax errors as connection timeouts", () => {
expect(isConnectionTimeoutErrorMessage("Query timed out after 30 seconds")).toBe(false);
expect(isConnectionTimeoutErrorMessage("PostgreSQL connection pool checkout timed out (5s)")).toBe(false);
expect(isConnectionTimeoutErrorMessage("Cancel request timed out after 10s.")).toBe(false);
expect(isConnectionTimeoutErrorMessage('syntax error at or near "select"')).toBe(false);
expect(
isConnectionTimeoutErrorMessage("Database operation timed out (stage: execute).", {
version: 1,
code: "DBX-JDBC-2002",
messageKey: "backendErrors.jdbc.operationTimedOut",
messageParams: { stage: "execute" },
source: "jdbcAgent",
operationOutcome: "unknown",
}),
).toBe(false);
});
});
describe("isQueryTimeoutErrorMessage", () => {
it("detects DBX query timeout messages", () => {
+14
View File
@@ -1,6 +1,20 @@
import type { BackendError } from "@/lib/backend/errorUtils";
const QUERY_TIMEOUT_STAGES = new Set(["execute", "fetch"]);
const CONNECTION_TIMEOUT_STAGES = new Set(["connect", "connection"]);
export function isConnectionTimeoutErrorMessage(message: string, backendError?: BackendError): boolean {
if (backendError?.messageKey === "backendErrors.jdbc.operationTimedOut") {
const stage = String(backendError.messageParams.stage ?? "").toLowerCase();
return CONNECTION_TIMEOUT_STAGES.has(stage);
}
const lower = message.toLowerCase();
if (/\btimeout occurred while creating a new object\b/.test(lower)) return true;
if (/\b(?:pool\s+checkout|checkout|metadata|loading|health check|cancel request)\b/.test(lower)) return false;
if (/\b(?:connection|connect|handshake)\b[\s\S]{0,80}\b(?:timed out|timeout expired|timeout exceeded|time-out)\b/.test(lower)) return true;
return /\b(?:timed out|timeout expired|timeout exceeded|time-out)\b[\s\S]{0,80}\b(?:connection attempt|connect|handshake)\b/.test(lower);
}
export function isQueryTimeoutErrorMessage(message: string, backendError?: BackendError): boolean {
if (backendError?.messageKey === "backendErrors.jdbc.operationTimedOut") {