fix(query): honor connection timeout during exports

This commit is contained in:
t8y2
2026-06-02 19:38:21 +08:00
parent 2bdbcec65b
commit ccaa50e36b
7 changed files with 85 additions and 25 deletions
@@ -72,6 +72,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
import { copyToClipboard } from "@/lib/clipboard";
import { formatSqlInsert } from "@/lib/exportFormats";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useQueryStore } from "@/stores/queryStore";
@@ -785,7 +786,10 @@ async function exportData(row: ObjectBrowserRow, format: "csv" | "json" | "sql")
schema,
tableName: row.name,
columns: queryColumns,
executePage: (sql) => api.executeQuery(props.connection.id, props.database, sql),
executePage: (sql) =>
api.executeQuery(props.connection.id, props.database, sql, undefined, undefined, {
timeoutSecs: queryTimeoutSecsForConnection(props.connection),
}),
});
if (format === "csv") {
@@ -848,7 +852,10 @@ async function exportDataXlsx(row: ObjectBrowserRow) {
schema,
tableName: row.name,
columns: queryColumns,
executePage: (sql) => api.executeQuery(props.connection.id, props.database, sql),
executePage: (sql) =>
api.executeQuery(props.connection.id, props.database, sql, undefined, undefined, {
timeoutSecs: queryTimeoutSecsForConnection(props.connection),
}),
});
let outputPath = `${row.name}.xlsx`;
@@ -90,6 +90,7 @@ import {
} from "@/lib/treeNodeClick";
import { formatSqlInsert } from "@/lib/exportFormats";
import { fetchTableDataForExport } from "@/lib/tableDataExport";
import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
import {
buildCreateDatabaseSql,
buildDuckDbAttachDatabaseSql,
@@ -2062,7 +2063,10 @@ async function exportData(format: "csv" | "json" | "sql") {
schema: node.schema,
tableName: node.label,
columns: queryColumns,
executePage: (sql) => api.executeQuery(connectionId, database, sql),
executePage: (sql) =>
api.executeQuery(connectionId, database, sql, undefined, undefined, {
timeoutSecs: queryTimeoutSecsForConnection(config),
}),
});
if (format === "csv") {
@@ -2132,7 +2136,10 @@ async function exportDataXlsx() {
schema: node.schema,
tableName: node.label,
columns: queryColumns,
executePage: (sql) => api.executeQuery(connectionId, database, sql),
executePage: (sql) =>
api.executeQuery(connectionId, database, sql, undefined, undefined, {
timeoutSecs: queryTimeoutSecsForConnection(config),
}),
});
let outputPath = `${node.label}.xlsx`;
+10
View File
@@ -0,0 +1,10 @@
import type { ConnectionConfig } from "@/types/database";
export const DEFAULT_QUERY_TIMEOUT_SECS = 30;
export function queryTimeoutSecsForConnection(
connection?: Pick<ConnectionConfig, "query_timeout_secs"> | null,
): number {
const value = Number(connection?.query_timeout_secs);
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_QUERY_TIMEOUT_SECS;
}
+27 -19
View File
@@ -23,6 +23,7 @@ import {
import { AGENT_DRIVER_TYPES } from "@/lib/databaseCapabilities";
import { editablePrimaryKeys } from "@/lib/tableEditing";
import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/tableDataExport";
import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout";
import * as api from "@/lib/api";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -705,12 +706,7 @@ export const useQueryStore = defineStore("query", () => {
await connStore.ensureConnected(tab.connectionId);
const conn = connStore.getConfig(tab.connectionId);
const useAgentCursor = !!conn?.db_type && AGENT_DRIVER_TYPES.has(conn.db_type);
const queryTimeoutSecs =
typeof conn?.query_timeout_secs === "number" &&
Number.isFinite(conn.query_timeout_secs) &&
conn.query_timeout_secs >= 0
? conn.query_timeout_secs
: 30;
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const settingsStore = useSettingsStore();
await previousResultSessionClose;
if (tab.mode === "query") {
@@ -910,13 +906,28 @@ export const useQueryStore = defineStore("query", () => {
...(clientSessionId ? { clientSessionId } : {}),
timeoutSecs: queryTimeoutSecs,
};
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const timeoutError = new Error(t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }));
const executionSchema = tab.mode === "data" ? undefined : tab.schema;
const results = await Promise.race([
api.executeMulti(tab.connectionId, tab.database, sqlToExecute, executionSchema, executionId, executionOptions),
new Promise<never>((_, reject) => setTimeout(() => reject(timeoutError), frontendTimeoutSecs * 1000)),
]);
const executionPromise = api.executeMulti(
tab.connectionId,
tab.database,
sqlToExecute,
executionSchema,
executionId,
executionOptions,
);
const results =
queryTimeoutSecs === 0
? await executionPromise
: await Promise.race([
executionPromise,
new Promise<never>((_, reject) => {
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
setTimeout(
() => reject(new Error(t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))),
frontendTimeoutSecs * 1000,
);
}),
]);
console.info("[DBX][executeTabSql:execute-multi:done]", {
traceId,
resultCount: results.length,
@@ -1033,12 +1044,7 @@ export const useQueryStore = defineStore("query", () => {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return { ok: false as const, reason: "empty" as const };
const conn = useConnectionStore().getConfig(tab.connectionId);
const queryTimeoutSecs =
typeof conn?.query_timeout_secs === "number" &&
Number.isFinite(conn.query_timeout_secs) &&
conn.query_timeout_secs >= 0
? conn.query_timeout_secs
: 30;
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const built = await buildExplainSql(databaseType, sql);
if (!built.ok) {
@@ -1193,6 +1199,7 @@ export const useQueryStore = defineStore("query", () => {
const connStore = useConnectionStore();
await connStore.ensureConnected(tab.connectionId);
const conn = connStore.getConfig(tab.connectionId);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const useAgentCursor = !!conn?.db_type && AGENT_DRIVER_TYPES.has(conn.db_type);
const queryBaseSql = tab.resultBaseSql ?? sql;
const pageLimit = Math.max(tab.resultPageLimit ?? 0, TABLE_DATA_EXPORT_PAGE_SIZE);
@@ -1220,8 +1227,9 @@ export const useQueryStore = defineStore("query", () => {
pageSize: plan.pageLimit,
resultSessionId: sessionId,
clientSessionId,
timeoutSecs: queryTimeoutSecs,
}
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit };
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, timeoutSecs: queryTimeoutSecs };
const results = await api.executeMulti(
tab.connectionId,
tab.database,
+9 -1
View File
@@ -281,7 +281,7 @@ impl ConnectionConfig {
if self.query_timeout_secs == 0 {
0
} else {
self.query_timeout_secs.clamp(1, 300)
self.query_timeout_secs.max(1)
}
}
@@ -1261,6 +1261,14 @@ mod tests {
assert_eq!(config.effective_query_timeout_secs(), 0);
}
#[test]
fn query_timeout_preserves_long_running_exports() {
let mut config = mysql_config("root", "", None);
config.query_timeout_secs = 3600;
assert_eq!(config.effective_query_timeout_secs(), 3600);
}
#[test]
fn mysql_url_encodes_oceanbase_username() {
let config = mysql_config("user@tenant#cluster", "secret", None);
+4 -1
View File
@@ -660,8 +660,9 @@ test("query result export fetches every paginated page", async () => {
const originalFetch = globalThis.fetch;
const preparedOffsets: number[] = [];
const executedSqls: string[] = [];
const timeoutSecs: unknown[] = [];
connectionStore.addEphemeralConnection(conn("conn-1"));
connectionStore.addEphemeralConnection({ ...conn("conn-1"), query_timeout_secs: 600 });
const tabId = store.createTab("conn-1", "db");
const tab = store.tabs.find((item) => item.id === tabId);
assert.ok(tab);
@@ -698,6 +699,7 @@ test("query result export fetches every paginated page", async () => {
if (url === "/api/query/execute-multi") {
const body = JSON.parse(String(init?.body ?? "{}"));
executedSqls.push(body.sql);
timeoutSecs.push(body.timeoutSecs);
const rows = String(body.sql).includes("offset:0")
? Array.from({ length: 10_000 }, (_, index) => [index + 1])
: [[10_001], [10_002]];
@@ -714,6 +716,7 @@ test("query result export fetches every paginated page", async () => {
assert.deepEqual(preparedOffsets, [0, 10_000]);
assert.deepEqual(executedSqls, ["select id from users /* offset:0 */", "select id from users /* offset:10000 */"]);
assert.deepEqual(timeoutSecs, [600, 600]);
assert.equal(exported?.rows.length, 10_002);
assert.deepEqual(exported?.rows.at(-1), [10_002]);
} finally {
+17
View File
@@ -0,0 +1,17 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import { DEFAULT_QUERY_TIMEOUT_SECS, queryTimeoutSecsForConnection } from "../../apps/desktop/src/lib/queryTimeout.ts";
test("uses default query timeout when connection has no setting", () => {
assert.equal(queryTimeoutSecsForConnection(undefined), DEFAULT_QUERY_TIMEOUT_SECS);
assert.equal(queryTimeoutSecsForConnection({}), DEFAULT_QUERY_TIMEOUT_SECS);
});
test("preserves long query timeout settings", () => {
assert.equal(queryTimeoutSecsForConnection({ query_timeout_secs: 600 }), 600);
assert.equal(queryTimeoutSecsForConnection({ query_timeout_secs: 3600 }), 3600);
});
test("allows zero to disable query timeout", () => {
assert.equal(queryTimeoutSecsForConnection({ query_timeout_secs: 0 }), 0);
});