mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(query): honor connection timeout during exports
This commit is contained in:
@@ -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`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user