mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(query): add timeout settings entry to error page
This commit is contained in:
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1143,6 +1143,7 @@ export default withEnglishFallback({
|
||||
pressToExecute: "{mod}+Enter で実行",
|
||||
pressToSaveSql: "{mod}+S でSQLを保存",
|
||||
queryTimeoutError: "クエリがタイムアウトしました({seconds}秒)。データベース接続が正常か確認してください。",
|
||||
changeConnectionTimeout: "接続タイムアウトを変更",
|
||||
changeQueryTimeout: "クエリタイムアウトを変更",
|
||||
connectionMayBeLost: "接続が失われた可能性があります。データを更新して再試行してください。",
|
||||
showResultsPane: "結果を表示",
|
||||
|
||||
@@ -1038,6 +1038,7 @@ export default withEnglishFallback({
|
||||
pressToExecute: "{mod}+Enter를 눌러 실행",
|
||||
pressToSaveSql: "{mod}+S를 눌러 SQL 저장",
|
||||
queryTimeoutError: "쿼리 시간 초과 ({seconds}초). 데이터베이스 연결이 정상인지 확인하세요.",
|
||||
changeConnectionTimeout: "연결 제한 시간 변경",
|
||||
changeQueryTimeout: "쿼리 제한 시간 변경",
|
||||
connectionMayBeLost: "연결이 끊어졌을 수 있습니다. 데이터를 새로고침하고 다시 시도하세요.",
|
||||
showResultsPane: "결과 표시",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1070,6 +1070,7 @@ export default withEnglishFallback({
|
||||
pressToExecute: "按 {mod}+Enter 执行查询",
|
||||
pressToSaveSql: "按 {mod}+S 保存 SQL",
|
||||
queryTimeoutError: "查询超时 ({seconds}s),请检查数据库连接是否正常",
|
||||
changeConnectionTimeout: "修改超时时间",
|
||||
changeQueryTimeout: "修改查询超时时间",
|
||||
connectionMayBeLost: "连接可能已断开,请刷新数据重试",
|
||||
showResultsPane: "显示结果",
|
||||
|
||||
@@ -1122,6 +1122,7 @@ export default withEnglishFallback({
|
||||
pressToExecute: "按 {mod}+Enter 執行查詢",
|
||||
pressToSaveSql: "按 {mod}+S 儲存 SQL",
|
||||
queryTimeoutError: "查詢逾時 ({seconds}s),請檢查資料庫連線是否正常",
|
||||
changeConnectionTimeout: "修改連線逾時時間",
|
||||
changeQueryTimeout: "修改查詢逾時時間",
|
||||
connectionMayBeLost: "連線可能已斷開,請重新整理資料重試",
|
||||
showResultsPane: "顯示結果",
|
||||
|
||||
+39
-1
@@ -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", () => {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user