feat(core): optimize query resource lifecycle

This commit is contained in:
t8y2 committed 2026-07-16 00:19:24 +08:00
1 parent b4bd6c42a7
commit 295b67dd36
29 files changed
+1857 -335

No files matched your search

@@ -311,7 +311,10 @@ const activeQueryError = computed(() => {
if (!result?.columns.includes("Error")) return "";
return String(result.rows[0]?.[0] ?? "");
});
const hasQueryOutput = computed(() => !!props.activeTab.result || !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true);
const hasQueryOutput = computed(
() =>
!!props.activeTab.result || props.activeTab.resultEvicted === true || !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true,
);
const visibleResultItems = computed(() => tabularResultItems(props.activeTab.results ?? (props.activeTab.result ? [props.activeTab.result] : undefined)));
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
const allResultExportSheets = computed(() =>
@@ -1196,6 +1199,12 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:cancelling="activeTab.isCancelling"
@cancel="emit('cancel')"
/>
<div v-else-if="activeTab.resultEvicted && activeTab.resultCacheState === 'missing'" class="flex flex-1 min-h-0 flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
<div>{{ t("editor.cachedResultUnavailable") }}</div>
<Button v-if="(activeTab.lastExecutedSql ?? activeTab.sql)?.trim()" variant="secondary" size="sm" @click="queryStore.reloadEvictedTab(activeTab.id, { reexecuteOnMissing: true })">
{{ t("editor.reexecuteQuery") }}
</Button>
</div>
<div v-else-if="!activeTab.result" class="flex-1 min-h-0 flex flex-col items-center justify-center gap-1 text-muted-foreground text-sm">
<div>{{ t("editor.pressToExecute", { mod: shortcutModifier }) }}</div>
<div>{{ t("editor.pressToSaveSql", { mod: shortcutModifier }) }}</div>
@@ -961,12 +961,20 @@ function openSidebarProcedure(node: TreeNode) {
sidebarProcedureOpen.value = true;
}
function openSidebarData(node: TreeNode, requireSelection: boolean, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>) {
function openSidebarData(node: TreeNode, requireSelection: boolean, openMode: "default" | "new-tab", runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>) {
const target = createSidebarActionTarget(node);
runSidebarDataOpenImmediately((request) => {
if (requireSelection && store.selectedTreeNodeId !== target.id) return;
return runner(target, request);
});
runSidebarDataOpenImmediately(
{
connectionKey: target.connectionId || target.id,
// Explicit new-tab opens are intentional independent work; ordinary
// navigation keeps latest-request-wins behavior.
supersede: openMode !== "new-tab",
},
(request) => {
if (requireSelection && store.selectedTreeNodeId !== target.id) return;
return runner(target, request);
},
);
}
function openSidebarVisibleDatabases(node: TreeNode) {
@@ -80,6 +80,7 @@ import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTem
import { connectionFilePath, defaultSqliteBackupFileName, isMemorySqlitePath, sqliteBackupSourcePath } from "@/lib/connection/connectionFile";
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
import { revealPathInFileManager } from "@/lib/backend/tauri";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
@@ -354,7 +355,7 @@ const emit = defineEmits<{
"open-ddl": [node: TreeNode];
"open-object-source": [node: TreeNode, initialEditing: boolean];
"open-procedure": [node: TreeNode];
"open-data": [node: TreeNode, requireSelection: boolean, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>];
"open-data": [node: TreeNode, requireSelection: boolean, openMode: DataTabOpenMode, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise<void>];
"open-visible-databases": [node: TreeNode];
"open-visible-schemas": [node: TreeNode];
"open-danger-dialog": [request: SidebarDangerDialogRequest];
@@ -1353,15 +1354,15 @@ async function openDamengJobAdmin() {
}
function scheduleOpenData(node: TreeNode) {
emit("open-data", node, true, openData);
emit("open-data", node, true, "default", openData);
}
function openDataImmediately(node: TreeNode = props.node) {
emit("open-data", node, false, openData);
emit("open-data", node, false, "default", openData);
}
function openDataInNewTabImmediately(node: TreeNode = props.node) {
emit("open-data", node, false, (target, request) => openData(target, request, "new-tab"));
emit("open-data", node, false, "new-tab", (target, request) => openData(target, request, "new-tab"));
}
async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMode: DataTabOpenMode = "default") {
@@ -1371,9 +1372,12 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
const startedAt = performance.now();
let lastPhaseAt = startedAt;
const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`;
const openDataLog = (level: "info" | "warn" | "error" | "debug", event: string, details: Record<string, unknown>) => {
appendDebugLog(level, `[DBX][openData:${event}]`, details);
};
const logPhase = (phase: string, extra: Record<string, unknown> = {}) => {
const now = performance.now();
console.info("[DBX][openData:phase]", {
openDataLog("info", "phase", {
traceId,
phase,
deltaMs: Math.round(now - lastPhaseAt),
@@ -1382,14 +1386,11 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
});
lastPhaseAt = now;
};
console.info("[DBX][openData:start]", {
openDataLog("info", "start", {
traceId,
type: node.type,
connectionId: node.connectionId,
database: node.database,
schema: node.schema,
table: node.label,
dbType: config?.db_type,
openMode,
});
const tableSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema);
const tableType = node.type === "view" ? "VIEW" : node.type === "materialized_view" ? "MATERIALIZED_VIEW" : (node.tableType ?? "TABLE");
@@ -1445,7 +1446,7 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
}
return queryStore.createTab(node.connectionId, node.database, node.label, "data", tableSchema);
})();
console.info("[DBX][openData:tab-created]", { traceId, tabId, elapsed: elapsed() });
openDataLog("info", "tab-created", { traceId, tabId, elapsed: elapsed() });
logPhase("tab-created", { tabId });
// Cancel any previous execution on this tab before starting a new one
@@ -1519,24 +1520,21 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
};
try {
console.info("[DBX][openData:ensure-connected:start]", { traceId, elapsed: elapsed() });
openDataLog("info", "ensure-connected:start", { traceId, elapsed: elapsed() });
await connectionStore.ensureConnected(node.connectionId);
if (!isActive()) {
logPhase("superseded-after-ensure-connected", { tabId });
return;
}
console.info("[DBX][openData:ensure-connected:done]", { traceId, elapsed: elapsed() });
openDataLog("info", "ensure-connected:done", { traceId, elapsed: elapsed() });
logPhase("ensure-connected", { tabId });
if (!config) throw new Error("Connection config not found");
const limit = tableOpenPageLimit();
const refreshTableMetaInBackground = async () => {
const metadataStartedAt = performance.now();
console.info("[DBX][openData:metadata:start]", {
openDataLog("info", "metadata:start", {
traceId,
database: node.database,
schema: querySchema,
table: node.label,
elapsed: elapsed(),
});
try {
@@ -1549,10 +1547,10 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
databaseType: metadataDatabaseType,
driverProfile: config.driver_profile || config.db_type,
catalog: node.catalog,
traceLogger: (event) => console.debug("[DBX][openData:metadata:trace]", { sourceTraceId: traceId, ...event }),
traceLogger: isDebugLoggingEnabled() ? (event) => openDataLog("debug", "metadata:trace", { sourceTraceId: traceId, ...event }) : undefined,
});
if (!isCurrentDataTab()) {
console.info("[DBX][openData:metadata:stale]", {
openDataLog("info", "metadata:stale", {
traceId,
tabId,
columnCount: loadedMetadata.metadata.columns.length,
@@ -1562,7 +1560,7 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
}
const nextTableMeta = tableMetadataToDataTabMeta(loadedMetadata.metadata, tableSchema);
queryStore.setTableMeta(tabId, nextTableMeta);
console.info("[DBX][openData:metadata:done]", {
openDataLog("info", "metadata:done", {
traceId,
tabId,
columnCount: nextTableMeta.columns.length,
@@ -1573,12 +1571,12 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
metadataMs: Math.round(performance.now() - metadataStartedAt),
});
} catch (error) {
console.warn("[DBX][openData:metadata:error]", { traceId, tabId, elapsed: elapsed(), error });
openDataLog("warn", "metadata:error", { traceId, tabId, elapsed: elapsed(), error });
}
};
const shouldRefreshTableMeta = !cachedTableMeta;
if (cachedTableMeta) {
console.info("[DBX][openData:metadata:cache-hit]", {
openDataLog("info", "metadata:cache-hit", {
traceId,
tabId,
columnCount: cachedTableMeta.columns.length,
@@ -1613,24 +1611,24 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
limit,
includeRowId,
});
console.info("[DBX][openData:sql-built]", {
openDataLog("info", "sql-built", {
traceId,
primaryKeys,
primaryKeyCount: primaryKeys.length,
includeRowId,
sql,
sqlLength: sql.length,
elapsed: elapsed(),
});
logPhase("sql-built", { tabId, columnCount: columns.length, primaryKeyCount: primaryKeys.length });
queryStore.updateSql(tabId, sql);
logPhase("sql-updated", { tabId });
console.info("[DBX][openData:execute:start]", { traceId, tabId, elapsed: elapsed() });
openDataLog("info", "execute:start", { traceId, tabId, elapsed: elapsed() });
await queryStore.executeTabSql(tabId, sql, {
sourceTraceId: traceId,
skipEnsureConnected: true,
pagination: { limit, offset: 0 },
});
console.info("[DBX][openData:execute:done]", { traceId, tabId, elapsed: elapsed() });
openDataLog("info", "execute:done", { traceId, tabId, elapsed: elapsed() });
logPhase("execute-tab-sql", { tabId });
if (shouldRefreshTableMeta && isCurrentDataTab()) {
void refreshTableMetaInBackground();
@@ -1641,7 +1639,7 @@ async function openData(node: TreeNode, request?: SidebarDataOpenRequest, openMo
logPhase("superseded-after-error", { tabId });
return;
}
console.error("[DBX][openData:error]", { traceId, elapsed: elapsed(), error: e });
openDataLog("error", "error", { traceId, elapsed: elapsed(), error: e });
logPhase("error", { tabId });
queryStore.setErrorResult(tabId, e);
}
+2
View File
@@ -1038,6 +1038,8 @@ export default {
noColumnsFound: "No columns found",
queryError: "Query Error",
dataUnavailable: "Table data needs to be reloaded.",
cachedResultUnavailable: "The cached result is missing or incompatible.",
reexecuteQuery: "Run query again",
dataUnavailableHintPrefix: "Press ",
dataUnavailableHintSuffix: " or click Refresh below to reload.",
refresh: "Refresh",
@@ -0,0 +1,57 @@
import type { QueryTab } from "@/types/database";
import type { TabResultSnapshot } from "@/lib/tabs/tabResultCache";
export function queryResultLifecycleSnapshot(): TabResultSnapshot {
const first = {
columns: ["id", "name"],
rows: [[1, "Ada"]],
affected_rows: 0,
execution_time_ms: 4,
sourceStatement: "SELECT id, name FROM users",
};
const second = {
columns: ["count"],
rows: [[1]],
affected_rows: 0,
execution_time_ms: 2,
sourceStatement: "SELECT COUNT(*) FROM users",
};
const resultRuns: NonNullable<QueryTab["resultRuns"]> = [
{
id: "run-1",
sequence: 1,
result: first,
results: [first, second],
activeResultIndex: 0,
resultLocalSortOriginalRows: [[1, "Ada"]],
resultBaseSql: "SELECT id, name FROM users",
resultPageSql: "SELECT id, name FROM users LIMIT 100 OFFSET 200",
resultPageLimit: 100,
resultPageOffset: 200,
tableMeta: {
schema: "public",
tableName: "users",
tableType: "TABLE",
columns: [{ name: "id", data_type: "integer", nullable: false }],
primaryKeys: ["id"],
},
},
];
return {
result: first,
results: [first, second],
activeResultIndex: 0,
resultLocalSortOriginalRows: [[1, "Ada"]],
resultRuns,
activeResultRunId: "run-1",
queryAnalysis: { tableName: "users", schema: "public", allowInsert: true, allowInsertDelete: true },
querySourceColumns: ["id", "name"],
tableMeta: resultRuns[0].tableMeta,
resultPageSql: "SELECT id, name FROM users LIMIT 100 OFFSET 200",
resultPageLimit: 100,
resultPageOffset: 200,
resultCountSql: "SELECT COUNT(*) FROM users",
resultTotalRowCount: 301,
cachedAt: 1,
};
}
@@ -1,95 +1,141 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately } from "@/lib/sidebar/sidebarDataOpenCoordinator";
function deferred() {
let resolve!: () => void;
const promise = new Promise<void>((done) => {
resolve = done;
});
return { promise, resolve };
}
describe("sidebarDataOpenCoordinator", () => {
afterEach(() => {
cancelPendingSidebarDataOpen();
});
it("cancels the active request when a newer open starts", async () => {
it("cancels the active request when a newer ordinary open starts", async () => {
const cancel = vi.fn();
const running = deferred();
let firstIsCurrent = true;
runSidebarDataOpenImmediately(async (request) => {
runSidebarDataOpenImmediately({ connectionKey: "conn" }, async (request) => {
request.registerCancel(cancel);
await Promise.resolve();
await running.promise;
firstIsCurrent = request.isCurrent();
});
runSidebarDataOpenImmediately(() => undefined);
runSidebarDataOpenImmediately({ connectionKey: "conn" }, () => undefined);
await Promise.resolve();
expect(cancel).toHaveBeenCalledOnce();
expect(firstIsCurrent).toBe(true);
running.resolve();
await Promise.resolve();
await Promise.resolve();
expect(firstIsCurrent).toBe(false);
});
it("replaces a superseded queued request before it starts", async () => {
const running = deferred();
const second = vi.fn();
const third = vi.fn();
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, () => running.promise);
runSidebarDataOpenImmediately({ connectionKey: "conn" }, second);
runSidebarDataOpenImmediately({ connectionKey: "conn" }, third);
running.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(second).not.toHaveBeenCalled();
expect(third).toHaveBeenCalledOnce();
});
it("bounds active work per connection", async () => {
const first = deferred();
const starts: string[] = [];
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, async () => {
starts.push("first");
await first.promise;
});
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, () => {
starts.push("second");
});
expect(starts).toEqual(["first"]);
first.resolve();
await vi.waitFor(() => expect(starts).toEqual(["first", "second"]));
});
it("allows different connections to make progress independently", () => {
const blocked = deferred();
const otherConnection = vi.fn();
runSidebarDataOpenImmediately({ connectionKey: "conn-a", supersede: false }, () => blocked.promise);
runSidebarDataOpenImmediately({ connectionKey: "conn-b", supersede: false }, otherConnection);
expect(otherConnection).toHaveBeenCalledOnce();
blocked.resolve();
});
it("keeps explicit new-tab opens independent while still queueing them", async () => {
const first = deferred();
const firstCancel = vi.fn();
const second = vi.fn();
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, async (request) => {
request.registerCancel(firstCancel);
await first.promise;
});
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, second);
expect(firstCancel).not.toHaveBeenCalled();
expect(second).not.toHaveBeenCalled();
first.resolve();
await vi.waitFor(() => expect(second).toHaveBeenCalledOnce());
});
it("runs registered cancellation immediately for an already stale request", async () => {
const cancel = vi.fn();
let registerLateCancel: (() => void) | undefined;
const running = deferred();
runSidebarDataOpenImmediately((request) => {
runSidebarDataOpenImmediately({ connectionKey: "conn" }, async (request) => {
registerLateCancel = () => request.registerCancel(cancel);
await running.promise;
});
runSidebarDataOpenImmediately(() => undefined);
runSidebarDataOpenImmediately({ connectionKey: "conn" }, () => undefined);
registerLateCancel?.();
await Promise.resolve();
expect(cancel).toHaveBeenCalledOnce();
running.resolve();
});
it("recovers after a runner throws synchronously", async () => {
const nextRunner = vi.fn();
runSidebarDataOpenImmediately(() => {
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, () => {
throw new Error("boom");
});
await Promise.resolve();
runSidebarDataOpenImmediately(nextRunner);
await Promise.resolve();
runSidebarDataOpenImmediately({ connectionKey: "conn", supersede: false }, nextRunner);
expect(nextRunner).toHaveBeenCalledOnce();
});
it("keeps only the newest rapid navigation request current", async () => {
const states: boolean[] = [];
const captures: Array<() => void> = [];
it("aborts a superseded request signal", () => {
let signal: AbortSignal | undefined;
for (let index = 0; index < 3; index += 1) {
runSidebarDataOpenImmediately((request) => {
captures.push(() => states.push(request.isCurrent()));
});
}
captures.forEach((capture) => capture());
expect(states).toEqual([false, false, true]);
});
it("does not cancel work that was not registered as sidebar-owned", async () => {
const sidebarCancel = vi.fn();
const unrelatedEditorCancel = vi.fn();
runSidebarDataOpenImmediately((request) => request.registerCancel(sidebarCancel));
runSidebarDataOpenImmediately(() => undefined);
await Promise.resolve();
expect(sidebarCancel).toHaveBeenCalledOnce();
expect(unrelatedEditorCancel).not.toHaveBeenCalled();
});
it("prevents a superseded connection completion from becoming current", async () => {
let finishConnection: (() => void) | undefined;
let completionWasCurrent = true;
runSidebarDataOpenImmediately(async (request) => {
await new Promise<void>((resolve) => {
finishConnection = resolve;
});
completionWasCurrent = request.isCurrent();
runSidebarDataOpenImmediately({ connectionKey: "conn" }, (request) => {
signal = request.signal;
});
runSidebarDataOpenImmediately(() => undefined);
finishConnection?.();
await Promise.resolve();
await Promise.resolve();
runSidebarDataOpenImmediately({ connectionKey: "conn" }, () => undefined);
expect(completionWasCurrent).toBe(false);
expect(signal?.aborted).toBe(true);
});
});
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { estimateQueryResultBytes, estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
function result(rows: unknown[][]) {
return { columns: ["value"], rows, affected_rows: 0, execution_time_ms: 1 };
}
describe("query result size estimation", () => {
it("grows with string and binary payloads", () => {
const small = estimateQueryResultBytes(result([["a"]]));
const large = estimateQueryResultBytes(result([["a".repeat(10_000)], [new Uint8Array(20_000)]]));
expect(large).toBeGreaterThan(small + 30_000);
});
it("counts multiple results and tolerates cyclic document values", () => {
const document: Record<string, unknown> = { name: "node" };
document.self = document;
const first = result([[document]]);
const second = result(Array.from({ length: 100 }, (_, index) => [index]));
expect(estimateQueryResultsBytes(undefined, [first, second])).toBeGreaterThan(estimateQueryResultBytes(first));
});
});
describe("inactive result byte budgets", () => {
it("evicts oldest small results to the count limit", () => {
const entries = Array.from({ length: 8 }, (_, index) => ({ id: `${index}`, estimatedBytes: 10, accessedAt: index }));
expect(selectInactiveResultEvictions(entries, 1_000, 5)).toEqual(["0", "1", "2"]);
});
it("evicts least-recently-used large results to the byte limit", () => {
const entries = [
{ id: "old", estimatedBytes: 80, accessedAt: 1 },
{ id: "middle", estimatedBytes: 70, accessedAt: 2 },
{ id: "new", estimatedBytes: 60, accessedAt: 3 },
];
expect(selectInactiveResultEvictions(entries, 100, 10)).toEqual(["old", "middle"]);
});
it("does not consider the active result even when it exceeds the budget", () => {
const inactive = [{ id: "inactive", estimatedBytes: 10, accessedAt: 1 }];
expect(selectInactiveResultEvictions(inactive, 100, 5)).toEqual([]);
});
});
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { encodeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
import { estimateQueryResultBytes } from "@/lib/tabs/queryResultSize";
import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, sidebarDataOpenDiagnostics } from "@/lib/sidebar/sidebarDataOpenCoordinator";
describe("resource lifecycle performance harness", () => {
it("records deterministic serialization and memory-estimation measurements", () => {
const result = {
columns: ["id", "label", "payload"],
rows: Array.from({ length: 25_000 }, (_, index) => [index, `row-${index}`, "x".repeat(64)]),
affected_rows: 0,
execution_time_ms: 1,
};
const estimateStartedAt = performance.now();
const estimatedBytes = estimateQueryResultBytes(result);
const estimateDurationMs = performance.now() - estimateStartedAt;
const serializeStartedAt = performance.now();
const encodedBytes = encodeTabResultSnapshot({ result, cachedAt: 1 }).byteLength;
const serializationDurationMs = performance.now() - serializeStartedAt;
console.info("[resource-lifecycle-baseline]", JSON.stringify({ rows: result.rows.length, estimatedBytes, encodedBytes, estimateDurationMs, serializationDurationMs }));
expect(estimatedBytes).toBeGreaterThan(encodedBytes);
expect(estimateDurationMs).toBeLessThan(250);
expect(serializationDurationMs).toBeLessThan(1_500);
});
});
describe("rapid table-open performance harness", () => {
it("coalesces a 100-request burst to one active and one latest queued request", async () => {
let release!: () => void;
const blocked = new Promise<void>((resolve) => {
release = resolve;
});
const committed: number[] = [];
const startedAt = performance.now();
runSidebarDataOpenImmediately({ connectionKey: "connection" }, async (request) => {
await blocked;
if (request.isCurrent()) committed.push(0);
});
for (let index = 1; index < 100; index += 1) {
runSidebarDataOpenImmediately({ connectionKey: "connection" }, (request) => {
if (request.isCurrent()) committed.push(index);
});
}
const enqueueDurationMs = performance.now() - startedAt;
const burstDiagnostics = sidebarDataOpenDiagnostics();
release();
await new Promise((resolve) => setTimeout(resolve, 0));
const totalDurationMs = performance.now() - startedAt;
console.info("[rapid-table-open-baseline]", JSON.stringify({ enqueueDurationMs, totalDurationMs, ...burstDiagnostics }));
expect(burstDiagnostics).toEqual({ activeTasks: 1, queuedTasks: 1, trackedRequests: 2 });
expect(committed).toEqual([99]);
cancelPendingSidebarDataOpen();
});
});
+91 -2
View File
@@ -1,7 +1,78 @@
import { describe, expect, it } from "vitest";
import { decodeTabResultSnapshot, encodeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
import { describe, expect, it, vi } from "vitest";
import { decodeTabResultSnapshot, encodeTabResultSnapshot, readResultCacheBackends, promoteFallbackResultCacheRead, resultCacheBackendOrder, resultCacheRuntimeConfig, selectResultCachePruneKeys, writeResultCacheBackends, type ResultCacheBackend } from "@/lib/tabs/tabResultCache";
import { queryResultLifecycleSnapshot } from "@/lib/__tests__/fixtures/queryResultLifecycle";
function backend(name: ResultCacheBackend["name"], overrides: Partial<ResultCacheBackend> = {}): ResultCacheBackend {
return {
name,
available: () => true,
read: async () => undefined,
write: async () => true,
delete: async () => undefined,
listMetadata: async () => [],
prune: async () => ({ deletedEntries: 0, deletedBytes: 0, orphanDeletions: 0, remainingEntries: 0, remainingBytes: 0 }),
deleteOwner: async () => undefined,
...overrides,
};
}
describe("tab result cache statement execution metadata", () => {
it("selects one authoritative backend before fallback", () => {
expect(resultCacheBackendOrder(true)).toEqual(["runtime", "indexed-db"]);
expect(resultCacheBackendOrder(false)).toEqual(["indexed-db", "runtime"]);
});
it("uses explicit browser runtime configuration", () => {
expect(resultCacheRuntimeConfig(false, { VITE_DBX_RESULT_CACHE_BACKEND: "http", VITE_DBX_RESULT_CACHE_FALLBACK: "true" } as ImportMetaEnv)).toEqual({
primary: "runtime",
fallbackEnabled: true,
});
});
it("writes only to the first successful backend", async () => {
const primaryWrite = vi.fn(async () => true);
const fallbackWrite = vi.fn(async () => true);
await writeResultCacheBackends([backend("runtime", { write: primaryWrite }), backend("indexed-db", { write: fallbackWrite })], "key", new Uint8Array([1]), { rowCount: 1, columnCount: 1 });
expect(primaryWrite).toHaveBeenCalledOnce();
expect(fallbackWrite).not.toHaveBeenCalled();
});
it("reads legacy fallback data for promotion", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const primary = backend("runtime");
const fallback = backend("indexed-db", { read: async () => bytes });
expect(await readResultCacheBackends([primary, fallback], "key")).toEqual({ bytes, backend: fallback });
});
it("promotes legacy IndexedDB or native bytes to the authoritative backend", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const primaryWrite = vi.fn(async () => true);
const primary = backend("runtime", { write: primaryWrite });
const legacy = backend("indexed-db");
expect(await promoteFallbackResultCacheRead([primary, legacy], "key", { bytes, backend: legacy }, { rowCount: 2, columnCount: 1 }, "connection-1")).toBe(true);
expect(primaryWrite).toHaveBeenCalledWith("key", bytes, { rowCount: 2, columnCount: 1 }, "connection-1");
});
it("skips an obsolete write before touching a backend", async () => {
const write = vi.fn(async () => true);
expect(await writeResultCacheBackends([backend("runtime", { write })], "key", new Uint8Array([1]), { rowCount: 1, columnCount: 1 }, () => false)).toBe(false);
expect(write).not.toHaveBeenCalled();
});
it("prunes crash leftovers and then unreferenced LRU entries", () => {
const selection = selectResultCachePruneKeys(
[
{ key: "live", rowCount: 1, columnCount: 1, byteSize: 8, createdAt: 1, lastAccessedAt: 1 },
{ key: "crash", rowCount: 1, columnCount: 1, byteSize: 7, createdAt: 1, lastAccessedAt: 1 },
{ key: "recent", rowCount: 1, columnCount: 1, byteSize: 6, createdAt: 9_999, lastAccessedAt: 9_999 },
],
{ liveKeys: ["live"], maxBytes: 10, orphanGraceMs: 100, maxAgeMs: 1_000 },
10_000,
);
expect(selection.keys).toEqual(["crash", "recent"]);
expect(selection.result).toMatchObject({ orphanDeletions: 1, remainingBytes: 8 });
});
it("preserves statement identity and the editor fingerprint", () => {
const encoded = encodeTabResultSnapshot({
results: [
@@ -35,4 +106,22 @@ describe("tab result cache statement execution metadata", () => {
{ statementIndex: 1, error: true },
]);
});
it("restores multi-result, pagination, local-sort, and editable metadata fixtures", () => {
const restored = decodeTabResultSnapshot(encodeTabResultSnapshot(queryResultLifecycleSnapshot()));
expect(restored?.results).toHaveLength(2);
expect(restored?.resultRuns?.[0]?.resultLocalSortOriginalRows).toEqual([[1, "Ada"]]);
expect(restored?.resultPageOffset).toBe(200);
expect(restored?.resultTotalRowCount).toBe(301);
expect(restored?.tableMeta?.primaryKeys).toEqual(["id"]);
expect(restored?.querySourceColumns).toEqual(["id", "name"]);
});
it("treats corrupt and unsupported snapshots as missing", () => {
expect(decodeTabResultSnapshot(new Uint8Array([0xff, 0x00]))).toBeUndefined();
const encoded = encodeTabResultSnapshot(queryResultLifecycleSnapshot());
encoded[0] = 0xff;
expect(decodeTabResultSnapshot(encoded)).toBeUndefined();
});
});
@@ -0,0 +1,25 @@
import type { QueryTab } from "@/types/database";
import { getResultCacheDiagnostics } from "@/lib/tabs/tabResultCache";
const cancellation = {
count: 0,
totalLatencyMs: 0,
maxLatencyMs: 0,
};
export function recordQueryCancellationLatency(durationMs: number) {
cancellation.count += 1;
cancellation.totalLatencyMs += durationMs;
cancellation.maxLatencyMs = Math.max(cancellation.maxLatencyMs, durationMs);
}
export function resourceLifecycleDiagnostics(tabs: readonly QueryTab[]) {
const cache = getResultCacheDiagnostics();
return {
activeTasks: tabs.filter((tab) => tab.isExecuting || tab.isExplaining || tab.isCancelling).length,
cancellationCount: cancellation.count,
averageCancellationLatencyMs: cancellation.count ? cancellation.totalLatencyMs / cancellation.count : 0,
maxCancellationLatencyMs: cancellation.maxLatencyMs,
...cache,
};
}
@@ -1,60 +1,166 @@
export interface SidebarDataOpenRequest {
isCurrent: () => boolean;
signal: AbortSignal;
registerCancel: (cancel: () => void | Promise<void>) => void;
}
export interface SidebarDataOpenOptions {
connectionKey: string;
supersede?: boolean;
}
type OpenDataRunner = (request: SidebarDataOpenRequest) => void | Promise<void>;
let generation = 0;
let activeGeneration = 0;
let activeCancel: (() => void | Promise<void>) | null = null;
interface QueuedOpen {
id: number;
connectionKey: string;
superseding: boolean;
controller: AbortController;
runner: OpenDataRunner;
cancel: (() => void | Promise<void>) | null;
started: boolean;
finished: boolean;
}
const DEFAULT_CONNECTION_KEY = "__sidebar__";
const MAX_ACTIVE_PER_CONNECTION = 1;
let nextRequestId = 0;
let latestSupersedingRequestId = 0;
const queuedByConnection = new Map<string, QueuedOpen[]>();
const activeCountByConnection = new Map<string, number>();
const requests = new Map<number, QueuedOpen>();
function runCancellation(cancel: (() => void | Promise<void>) | null) {
if (!cancel) return;
void Promise.resolve(cancel()).catch(() => undefined);
}
function supersedeCurrentRequest(): number {
generation += 1;
runCancellation(activeCancel);
activeCancel = null;
activeGeneration = 0;
return generation;
function isCurrent(request: QueuedOpen): boolean {
if (request.finished || request.controller.signal.aborted) return false;
return !request.superseding || request.id === latestSupersedingRequestId;
}
function executeRequest(requestGeneration: number, runner: OpenDataRunner) {
if (requestGeneration !== generation) return;
const request: SidebarDataOpenRequest = {
isCurrent: () => requestGeneration === generation,
function removeQueuedRequest(request: QueuedOpen) {
const queue = queuedByConnection.get(request.connectionKey);
if (!queue) return;
const index = queue.indexOf(request);
if (index >= 0) queue.splice(index, 1);
if (queue.length === 0) queuedByConnection.delete(request.connectionKey);
}
function cancelRequest(request: QueuedOpen) {
if (request.finished) return;
request.controller.abort();
runCancellation(request.cancel);
request.cancel = null;
if (!request.started) {
request.finished = true;
removeQueuedRequest(request);
requests.delete(request.id);
}
}
function supersedeCurrentRequest() {
const current = requests.get(latestSupersedingRequestId);
if (current) cancelRequest(current);
}
function finishRequest(request: QueuedOpen) {
if (request.finished) return;
request.finished = true;
request.cancel = null;
requests.delete(request.id);
const activeCount = activeCountByConnection.get(request.connectionKey) ?? 0;
if (activeCount <= 1) activeCountByConnection.delete(request.connectionKey);
else activeCountByConnection.set(request.connectionKey, activeCount - 1);
drainConnection(request.connectionKey);
}
function executeRequest(request: QueuedOpen) {
if (!isCurrent(request)) {
cancelRequest(request);
return;
}
request.started = true;
activeCountByConnection.set(request.connectionKey, (activeCountByConnection.get(request.connectionKey) ?? 0) + 1);
const openRequest: SidebarDataOpenRequest = {
isCurrent: () => isCurrent(request),
signal: request.controller.signal,
registerCancel: (cancel) => {
if (requestGeneration !== generation) {
if (!isCurrent(request)) {
runCancellation(cancel);
return;
}
activeGeneration = requestGeneration;
activeCancel = cancel;
request.cancel = cancel;
},
};
let result: void | Promise<void>;
try {
result = runner(request);
result = request.runner(openRequest);
} catch {
result = undefined;
}
void Promise.resolve(result)
.catch(() => undefined)
.finally(() => {
if (activeGeneration !== requestGeneration) return;
activeGeneration = 0;
activeCancel = null;
});
.finally(() => finishRequest(request));
}
export function runSidebarDataOpenImmediately(runner: OpenDataRunner) {
const requestGeneration = supersedeCurrentRequest();
executeRequest(requestGeneration, runner);
function drainConnection(connectionKey: string) {
if ((activeCountByConnection.get(connectionKey) ?? 0) >= MAX_ACTIVE_PER_CONNECTION) return;
const queue = queuedByConnection.get(connectionKey);
const request = queue?.shift();
if (!request) {
queuedByConnection.delete(connectionKey);
return;
}
if (queue?.length === 0) queuedByConnection.delete(connectionKey);
executeRequest(request);
}
function enqueueRequest(options: SidebarDataOpenOptions, runner: OpenDataRunner) {
const superseding = options.supersede !== false;
if (superseding) supersedeCurrentRequest();
const request: QueuedOpen = {
id: ++nextRequestId,
connectionKey: options.connectionKey || DEFAULT_CONNECTION_KEY,
superseding,
controller: new AbortController(),
runner,
cancel: null,
started: false,
finished: false,
};
if (superseding) latestSupersedingRequestId = request.id;
requests.set(request.id, request);
const queue = queuedByConnection.get(request.connectionKey) ?? [];
queue.push(request);
queuedByConnection.set(request.connectionKey, queue);
drainConnection(request.connectionKey);
}
export function runSidebarDataOpenImmediately(runner: OpenDataRunner): void;
export function runSidebarDataOpenImmediately(options: SidebarDataOpenOptions, runner: OpenDataRunner): void;
export function runSidebarDataOpenImmediately(optionsOrRunner: SidebarDataOpenOptions | OpenDataRunner, maybeRunner?: OpenDataRunner) {
if (typeof optionsOrRunner === "function") {
enqueueRequest({ connectionKey: DEFAULT_CONNECTION_KEY }, optionsOrRunner);
return;
}
if (maybeRunner) enqueueRequest(optionsOrRunner, maybeRunner);
}
export function cancelPendingSidebarDataOpen() {
supersedeCurrentRequest();
for (const request of [...requests.values()]) cancelRequest(request);
latestSupersedingRequestId = 0;
}
export function sidebarDataOpenDiagnostics() {
return {
activeTasks: [...activeCountByConnection.values()].reduce((total, count) => total + count, 0),
queuedTasks: [...queuedByConnection.values()].reduce((total, queue) => total + queue.length, 0),
trackedRequests: requests.size,
};
}
@@ -0,0 +1,51 @@
import type { QueryResult } from "@/types/database";
const ARRAY_OVERHEAD_BYTES = 24;
const OBJECT_OVERHEAD_BYTES = 32;
const VALUE_SLOT_BYTES = 8;
function estimateValueBytes(value: unknown, seen: WeakSet<object>): number {
if (value == null) return VALUE_SLOT_BYTES;
if (typeof value === "string") return VALUE_SLOT_BYTES + value.length * 2;
if (typeof value === "number" || typeof value === "bigint") return 16;
if (typeof value === "boolean") return 8;
if (value instanceof Uint8Array) return OBJECT_OVERHEAD_BYTES + value.byteLength;
if (value instanceof ArrayBuffer) return OBJECT_OVERHEAD_BYTES + value.byteLength;
if (value instanceof Date) return OBJECT_OVERHEAD_BYTES;
if (typeof value !== "object") return VALUE_SLOT_BYTES;
if (seen.has(value)) return VALUE_SLOT_BYTES;
seen.add(value);
if (Array.isArray(value)) {
return ARRAY_OVERHEAD_BYTES + value.reduce((total, item) => total + estimateValueBytes(item, seen), 0);
}
return OBJECT_OVERHEAD_BYTES + Object.entries(value).reduce((total, [key, entry]) => total + key.length * 2 + estimateValueBytes(entry, seen), 0);
}
export function estimateQueryResultBytes(result: QueryResult | undefined): number {
if (!result) return 0;
return estimateValueBytes(result, new WeakSet<object>());
}
export function estimateQueryResultsBytes(result: QueryResult | undefined, results: QueryResult[] | undefined): number {
if (results?.length) return results.reduce((total, item) => total + estimateQueryResultBytes(item), ARRAY_OVERHEAD_BYTES);
return estimateQueryResultBytes(result);
}
export interface InactiveResultResidency {
id: string;
estimatedBytes: number;
accessedAt: number;
}
export function selectInactiveResultEvictions(entries: InactiveResultResidency[], maxBytes: number, maxCount: number): string[] {
const ordered = [...entries].sort((left, right) => left.accessedAt - right.accessedAt);
let bytes = ordered.reduce((total, entry) => total + entry.estimatedBytes, 0);
const evicted: string[] = [];
while (ordered.length - evicted.length > maxCount || bytes > maxBytes) {
const entry = ordered[evicted.length];
if (!entry) break;
evicted.push(entry.id);
bytes -= entry.estimatedBytes;
}
return evicted;
}
+366 -54
View File
@@ -5,8 +5,12 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { apiUrl } from "@/lib/common/webPath";
const DB_NAME = "dbx-tab-runtime-cache";
const DB_VERSION = 1;
const DB_VERSION = 2;
const RESULT_STORE = "resultSnapshots";
const RESULT_METADATA_STORE = "resultSnapshotMetadata";
const DEFAULT_PERSISTENT_CACHE_BYTES = 512 * 1024 * 1024;
const DEFAULT_ORPHAN_GRACE_MS = 24 * 60 * 60 * 1000;
const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const PAYLOAD_MAGIC = "DBX_TAB_RESULT_CACHE";
const PAYLOAD_VERSION = 1;
const PAYLOAD_CODEC = "msgpack-columnar";
@@ -80,6 +84,58 @@ interface TabResultCacheEnvelope {
payload: TabResultSnapshotPayload;
}
export type ResultCacheBackendName = "indexed-db" | "runtime";
export interface ResultCacheMetadata {
key: string;
rowCount: number;
columnCount: number;
byteSize: number;
createdAt: number;
lastAccessedAt: number;
ownerId?: string;
}
export interface ResultCachePruneOptions {
liveKeys: string[];
maxBytes: number;
orphanGraceMs: number;
maxAgeMs?: number;
}
export interface ResultCachePruneResult {
deletedEntries: number;
deletedBytes: number;
orphanDeletions: number;
remainingEntries: number;
remainingBytes: number;
}
export interface ResultCacheBackend {
name: ResultCacheBackendName;
available: () => boolean;
read: (key: string) => Promise<Uint8Array | undefined>;
write: (key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, ownerId?: string) => Promise<boolean>;
delete: (key: string) => Promise<void>;
listMetadata: () => Promise<ResultCacheMetadata[]>;
prune: (options: ResultCachePruneOptions) => Promise<ResultCachePruneResult>;
deleteOwner: (ownerId: string) => Promise<void>;
}
const resultCacheDiagnostics = {
cacheBytes: 0,
evictions: 0,
serializationCount: 0,
serializationDurationMs: 0,
serializedBytes: 0,
orphanDeletions: 0,
corruptSnapshots: 0,
};
export function getResultCacheDiagnostics() {
return { ...resultCacheDiagnostics };
}
function indexedDb(): IDBFactory | undefined {
return typeof globalThis.indexedDB === "undefined" ? undefined : globalThis.indexedDB;
}
@@ -118,6 +174,7 @@ function openCacheDb(): Promise<IDBDatabase | null> {
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(RESULT_STORE)) db.createObjectStore(RESULT_STORE);
if (!db.objectStoreNames.contains(RESULT_METADATA_STORE)) db.createObjectStore(RESULT_METADATA_STORE);
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => {
@@ -129,6 +186,129 @@ function openCacheDb(): Promise<IDBDatabase | null> {
return dbPromise;
}
async function writeIndexedDbCache(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, ownerId?: string): Promise<boolean> {
const db = await openCacheDb();
if (!db) return false;
const previous = (await requestToPromise(db.transaction(RESULT_METADATA_STORE, "readonly").objectStore(RESULT_METADATA_STORE).get(key))) as ResultCacheMetadata | undefined;
const transaction = db.transaction([RESULT_STORE, RESULT_METADATA_STORE], "readwrite");
const metadataStore = transaction.objectStore(RESULT_METADATA_STORE);
const now = Date.now();
await Promise.all([
requestToPromise(transaction.objectStore(RESULT_STORE).put(bytes, key)),
requestToPromise(
metadataStore.put(
{
key,
rowCount: stats.rowCount,
columnCount: stats.columnCount,
byteSize: bytes.byteLength,
createdAt: previous?.createdAt ?? now,
lastAccessedAt: now,
ownerId,
} satisfies ResultCacheMetadata,
key,
),
),
]);
return true;
}
async function readIndexedDbCache(key: string): Promise<Uint8Array | undefined> {
const db = await openCacheDb();
if (!db) return undefined;
const value = await requestToPromise(db.transaction(RESULT_STORE, "readonly").objectStore(RESULT_STORE).get(key));
if (isBinaryPayload(value)) {
const previous = (await requestToPromise(db.transaction(RESULT_METADATA_STORE, "readonly").objectStore(RESULT_METADATA_STORE).get(key))) as ResultCacheMetadata | undefined;
const store = db.transaction(RESULT_METADATA_STORE, "readwrite").objectStore(RESULT_METADATA_STORE);
const now = Date.now();
await requestToPromise(store.put(previous ? { ...previous, lastAccessedAt: now } : { key, rowCount: 0, columnCount: 0, byteSize: value.byteLength, createdAt: now, lastAccessedAt: now }, key));
}
return isBinaryPayload(value) ? new Uint8Array(value) : undefined;
}
async function deleteIndexedDbCache(key: string): Promise<void> {
const db = await openCacheDb();
if (!db) return;
const transaction = db.transaction([RESULT_STORE, RESULT_METADATA_STORE], "readwrite");
await Promise.all([requestToPromise(transaction.objectStore(RESULT_STORE).delete(key)), requestToPromise(transaction.objectStore(RESULT_METADATA_STORE).delete(key))]);
}
async function listIndexedDbCacheMetadata(): Promise<ResultCacheMetadata[]> {
const db = await openCacheDb();
if (!db) return [];
const transaction = db.transaction([RESULT_STORE, RESULT_METADATA_STORE], "readonly");
const metadataRequest = transaction.objectStore(RESULT_METADATA_STORE).getAll();
const keysRequest = transaction.objectStore(RESULT_STORE).getAllKeys();
const [metadata, keys] = await Promise.all([requestToPromise(metadataRequest) as Promise<ResultCacheMetadata[]>, requestToPromise(keysRequest)]);
const byKey = new Map(metadata.map((entry) => [entry.key, entry]));
const missingKeys = keys.map(String).filter((key) => !byKey.has(key));
const migratedMetadata: ResultCacheMetadata[] = [];
for (const key of missingKeys) {
const value = await requestToPromise(db.transaction(RESULT_STORE, "readonly").objectStore(RESULT_STORE).get(key));
const now = Date.now();
const entry: ResultCacheMetadata = {
key,
rowCount: 0,
columnCount: 0,
byteSize: isBinaryPayload(value) ? value.byteLength : 0,
createdAt: now,
lastAccessedAt: now,
};
byKey.set(key, entry);
migratedMetadata.push(entry);
}
if (migratedMetadata.length) {
const migration = db.transaction(RESULT_METADATA_STORE, "readwrite").objectStore(RESULT_METADATA_STORE);
await Promise.all(migratedMetadata.map((entry) => requestToPromise(migration.put(entry, entry.key))));
}
return [...byKey.values()];
}
export function selectResultCachePruneKeys(metadata: ResultCacheMetadata[], options: ResultCachePruneOptions, now = Date.now()): { keys: string[]; result: ResultCachePruneResult } {
const live = new Set(options.liveKeys);
const ordered = [...metadata].sort((left, right) => left.lastAccessedAt - right.lastAccessedAt || left.key.localeCompare(right.key));
const deleted = new Set<string>();
let remainingBytes = ordered.reduce((total, entry) => total + entry.byteSize, 0);
let orphanDeletions = 0;
for (const entry of ordered) {
if (live.has(entry.key)) continue;
const orphanExpired = now - entry.createdAt >= Math.max(0, options.orphanGraceMs);
const ageExpired = options.maxAgeMs !== undefined && now - entry.lastAccessedAt >= Math.max(0, options.maxAgeMs);
if (!orphanExpired && !ageExpired) continue;
deleted.add(entry.key);
remainingBytes -= entry.byteSize;
if (orphanExpired) orphanDeletions += 1;
}
for (const entry of ordered) {
if (remainingBytes <= Math.max(0, options.maxBytes)) break;
if (live.has(entry.key) || deleted.has(entry.key)) continue;
deleted.add(entry.key);
remainingBytes -= entry.byteSize;
}
const deletedBytes = ordered.filter((entry) => deleted.has(entry.key)).reduce((total, entry) => total + entry.byteSize, 0);
return {
keys: [...deleted],
result: {
deletedEntries: deleted.size,
deletedBytes,
orphanDeletions,
remainingEntries: ordered.length - deleted.size,
remainingBytes,
},
};
}
async function pruneIndexedDbCache(options: ResultCachePruneOptions): Promise<ResultCachePruneResult> {
const selection = selectResultCachePruneKeys(await listIndexedDbCacheMetadata(), options);
await Promise.all(selection.keys.map(deleteIndexedDbCache));
return selection.result;
}
async function deleteIndexedDbCacheOwner(ownerId: string): Promise<void> {
const metadata = await listIndexedDbCacheMetadata();
await Promise.all(metadata.filter((entry) => entry.ownerId === ownerId).map((entry) => deleteIndexedDbCache(entry.key)));
}
function clonePlain<T>(value: T): T {
const raw = toRaw(value);
if (typeof structuredClone === "function") return structuredClone(raw);
@@ -272,7 +452,7 @@ function canUseRemoteRuntimeCache(): boolean {
return typeof btoa !== "undefined" && typeof atob !== "undefined" && (isTauriRuntime() || typeof fetch !== "undefined");
}
async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }): Promise<boolean> {
async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, ownerId?: string): Promise<boolean> {
if (!canUseRemoteRuntimeCache()) return false;
try {
if (isTauriRuntime()) {
@@ -282,6 +462,7 @@ async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: {
payloadBase64: bytesToBase64(bytes),
rowCount: stats.rowCount,
columnCount: stats.columnCount,
ownerId,
});
return true;
}
@@ -293,6 +474,7 @@ async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: {
payloadBase64: bytesToBase64(bytes),
rowCount: stats.rowCount,
columnCount: stats.columnCount,
ownerId,
}),
});
return response.ok;
@@ -302,6 +484,53 @@ async function writeRemoteRuntimeCache(key: string, bytes: Uint8Array, stats: {
}
}
function normalizeRuntimeMetadata(entry: Record<string, unknown>): ResultCacheMetadata {
return {
key: String(entry.key ?? ""),
rowCount: Number(entry.rowCount ?? 0),
columnCount: Number(entry.columnCount ?? 0),
byteSize: Number(entry.byteSize ?? 0),
createdAt: Number(entry.createdAt ?? 0),
lastAccessedAt: Number(entry.lastAccessedAt ?? 0),
ownerId: typeof entry.ownerId === "string" ? entry.ownerId : undefined,
};
}
async function listRemoteRuntimeCacheMetadata(): Promise<ResultCacheMetadata[]> {
if (!canUseRemoteRuntimeCache()) return [];
if (isTauriRuntime()) {
const { invoke } = await import("@tauri-apps/api/core");
const entries = await invoke<Record<string, unknown>[]>("list_tab_runtime_cache_metadata");
return entries.map(normalizeRuntimeMetadata);
}
const response = await fetch(apiUrl("/api/tab-runtime-cache/metadata"));
if (!response.ok) return [];
return ((await response.json()) as Record<string, unknown>[]).map(normalizeRuntimeMetadata);
}
async function pruneRemoteRuntimeCache(options: ResultCachePruneOptions): Promise<ResultCachePruneResult> {
if (isTauriRuntime()) {
const { invoke } = await import("@tauri-apps/api/core");
return invoke<ResultCachePruneResult>("prune_tab_runtime_cache", { request: options });
}
const response = await fetch(apiUrl("/api/tab-runtime-cache/prune"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options),
});
if (!response.ok) throw new Error(`Runtime cache prune failed: ${response.status}`);
return response.json() as Promise<ResultCachePruneResult>;
}
async function deleteRemoteRuntimeCacheOwner(ownerId: string): Promise<void> {
if (isTauriRuntime()) {
const { invoke } = await import("@tauri-apps/api/core");
await invoke("delete_tab_runtime_cache_owner", { ownerId });
return;
}
await fetch(apiUrl(`/api/tab-runtime-cache/owner?owner_id=${encodeURIComponent(ownerId)}`), { method: "DELETE" });
}
async function readRemoteRuntimeCache(key: string): Promise<Uint8Array | undefined> {
if (!canUseRemoteRuntimeCache()) return undefined;
try {
@@ -334,12 +563,82 @@ async function deleteRemoteRuntimeCache(key: string): Promise<void> {
}
}
function scheduleRemoteRuntimeCacheWrite(key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, version: number) {
window.setTimeout(async () => {
if (!isCurrentCacheKeyVersion(key, version)) return;
await writeRemoteRuntimeCache(key, bytes, stats);
clearCacheKeyVersionIfCurrent(key, version);
}, 0);
const indexedDbBackend: ResultCacheBackend = {
name: "indexed-db",
available: () => indexedDb() !== undefined,
read: readIndexedDbCache,
write: writeIndexedDbCache,
delete: deleteIndexedDbCache,
listMetadata: listIndexedDbCacheMetadata,
prune: pruneIndexedDbCache,
deleteOwner: deleteIndexedDbCacheOwner,
};
const runtimeBackend: ResultCacheBackend = {
name: "runtime",
available: canUseRemoteRuntimeCache,
read: readRemoteRuntimeCache,
write: writeRemoteRuntimeCache,
delete: deleteRemoteRuntimeCache,
listMetadata: listRemoteRuntimeCacheMetadata,
prune: pruneRemoteRuntimeCache,
deleteOwner: deleteRemoteRuntimeCacheOwner,
};
export interface ResultCacheRuntimeConfig {
primary: ResultCacheBackendName;
fallbackEnabled: boolean;
}
export function resultCacheRuntimeConfig(tauriRuntime = isTauriRuntime(), environment = import.meta.env): ResultCacheRuntimeConfig {
const configured = environment.VITE_DBX_RESULT_CACHE_BACKEND;
const primary = tauriRuntime || configured === "http" || configured === "runtime" ? "runtime" : "indexed-db";
return { primary, fallbackEnabled: environment.VITE_DBX_RESULT_CACHE_FALLBACK !== "false" };
}
export function resultCacheBackendOrder(tauriRuntime = isTauriRuntime(), config = resultCacheRuntimeConfig(tauriRuntime)): ResultCacheBackendName[] {
return config.primary === "runtime" ? ["runtime", "indexed-db"] : ["indexed-db", "runtime"];
}
function availableResultCacheBackends(includeLegacyFallback = true): ResultCacheBackend[] {
const byName: Record<ResultCacheBackendName, ResultCacheBackend> = {
"indexed-db": indexedDbBackend,
runtime: runtimeBackend,
};
const config = resultCacheRuntimeConfig();
const order = resultCacheBackendOrder(isTauriRuntime(), config);
const selected = includeLegacyFallback || config.fallbackEnabled ? order : order.slice(0, 1);
return selected.map((name) => byName[name]).filter((backend) => backend.available());
}
export async function writeResultCacheBackends(backends: ResultCacheBackend[], key: string, bytes: Uint8Array, stats: { rowCount: number; columnCount: number }, isCurrent: () => boolean = () => true, ownerId?: string): Promise<boolean> {
for (const backend of backends) {
if (!isCurrent()) return false;
try {
if (await backend.write(key, bytes, stats, ownerId)) return true;
} catch (error) {
console.warn("[DBX][tab-result-cache:write:error]", { key, backend: backend.name, error });
}
}
return false;
}
export async function readResultCacheBackends(backends: ResultCacheBackend[], key: string): Promise<{ bytes: Uint8Array; backend: ResultCacheBackend } | undefined> {
for (const backend of backends) {
try {
const bytes = await backend.read(key);
if (bytes) return { bytes, backend };
} catch (error) {
console.warn("[DBX][tab-result-cache:read:error]", { key, backend: backend.name, error });
}
}
return undefined;
}
export async function promoteFallbackResultCacheRead(backends: ResultCacheBackend[], key: string, cached: { bytes: Uint8Array; backend: ResultCacheBackend }, stats: { rowCount: number; columnCount: number }, ownerId?: string): Promise<boolean> {
const primary = backends[0];
if (!primary || cached.backend === primary) return false;
return primary.write(key, cached.bytes, stats, ownerId);
}
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -375,7 +674,13 @@ export function encodeTabResultSnapshot(snapshot: TabResultSnapshot): Uint8Array
}
export function decodeTabResultSnapshot(bytes: Uint8Array | ArrayBuffer): TabResultSnapshot | undefined {
const decoded = decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
let decoded: unknown;
try {
decoded = decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
} catch {
resultCacheDiagnostics.corruptSnapshots += 1;
return undefined;
}
if (!isRecord(decoded)) return undefined;
if (decoded.magic !== PAYLOAD_MAGIC || decoded.version !== PAYLOAD_VERSION || decoded.codec !== PAYLOAD_CODEC) {
return undefined;
@@ -413,57 +718,48 @@ export function buildTabResultSnapshot(tab: QueryTab): TabResultSnapshot | undef
};
}
export async function writeTabResultSnapshot(key: string, snapshot: TabResultSnapshot | undefined): Promise<boolean> {
function afterActiveResultPaint(): Promise<void> {
if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") return Promise.resolve();
return new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
resolve();
};
requestAnimationFrame(finish);
// Background WebViews and test DOMs may throttle animation frames indefinitely.
setTimeout(finish, 100);
});
}
export async function writeTabResultSnapshot(key: string, snapshot: TabResultSnapshot | undefined, ownerId?: string): Promise<boolean> {
if (!snapshot) return false;
const version = bumpCacheKeyVersion(key);
const encoded = encodeTabResultSnapshot(snapshot);
const stats = resultStats(snapshot);
let wroteLocal = false;
try {
const db = await openCacheDb();
if (db) {
const tx = db.transaction(RESULT_STORE, "readwrite");
await requestToPromise(tx.objectStore(RESULT_STORE).put(encoded, key));
wroteLocal = true;
}
} catch (error) {
console.warn("[DBX][tab-result-cache:write:error]", { key, error });
}
if (wroteLocal) {
if (typeof window === "undefined") {
void writeRemoteRuntimeCache(key, encoded, stats).finally(() => clearCacheKeyVersionIfCurrent(key, version));
} else {
scheduleRemoteRuntimeCacheWrite(key, encoded, stats, version);
}
return true;
}
await afterActiveResultPaint();
if (!isCurrentCacheKeyVersion(key, version)) return false;
const startedAt = typeof performance === "undefined" ? Date.now() : performance.now();
const encoded = encodeTabResultSnapshot(snapshot);
const finishedAt = typeof performance === "undefined" ? Date.now() : performance.now();
resultCacheDiagnostics.serializationCount += 1;
resultCacheDiagnostics.serializationDurationMs += finishedAt - startedAt;
resultCacheDiagnostics.serializedBytes += encoded.byteLength;
const stats = resultStats(snapshot);
try {
return await writeRemoteRuntimeCache(key, encoded, stats);
return await writeResultCacheBackends(availableResultCacheBackends(false), key, encoded, stats, () => isCurrentCacheKeyVersion(key, version), ownerId);
} finally {
clearCacheKeyVersionIfCurrent(key, version);
}
}
export async function readTabResultSnapshot(key: string): Promise<TabResultSnapshot | undefined> {
try {
const db = await openCacheDb();
if (db) {
const value = await requestToPromise(db.transaction(RESULT_STORE, "readonly").objectStore(RESULT_STORE).get(key));
if (isBinaryPayload(value)) return decodeTabResultSnapshot(value);
if (value) return value as TabResultSnapshot;
}
const remoteBytes = await readRemoteRuntimeCache(key);
const remoteSnapshot = remoteBytes ? decodeTabResultSnapshot(remoteBytes) : undefined;
if (remoteBytes && remoteSnapshot && db) {
void requestToPromise(db.transaction(RESULT_STORE, "readwrite").objectStore(RESULT_STORE).put(remoteBytes, key));
}
return remoteSnapshot;
} catch (error) {
console.warn("[DBX][tab-result-cache:read:error]", { key, error });
const remoteBytes = await readRemoteRuntimeCache(key);
return remoteBytes ? decodeTabResultSnapshot(remoteBytes) : undefined;
}
const backends = availableResultCacheBackends(true);
const cached = await readResultCacheBackends(backends, key);
if (!cached) return undefined;
const snapshot = decodeTabResultSnapshot(cached.bytes);
if (!snapshot) return undefined;
void promoteFallbackResultCacheRead(backends, key, cached, resultStats(snapshot));
return snapshot;
}
export async function deleteTabResultSnapshot(key: string): Promise<void> {
@@ -474,13 +770,29 @@ export async function deleteTabResultSnapshot(key: string): Promise<void> {
else setTimeout(cleanup, 5000);
};
try {
const db = await openCacheDb();
if (db) await requestToPromise(db.transaction(RESULT_STORE, "readwrite").objectStore(RESULT_STORE).delete(key));
await deleteRemoteRuntimeCache(key);
await Promise.all(availableResultCacheBackends().map((backend) => backend.delete(key)));
clearVersionLater();
} catch (error) {
console.warn("[DBX][tab-result-cache:delete:error]", { key, error });
await deleteRemoteRuntimeCache(key);
clearVersionLater();
}
}
export async function pruneTabResultSnapshots(liveKeys: Iterable<string>, options: Partial<Omit<ResultCachePruneOptions, "liveKeys">> = {}): Promise<ResultCachePruneResult | undefined> {
const primary = availableResultCacheBackends(false)[0];
if (!primary) return undefined;
const result = await primary.prune({
liveKeys: [...new Set(liveKeys)],
maxBytes: options.maxBytes ?? DEFAULT_PERSISTENT_CACHE_BYTES,
orphanGraceMs: options.orphanGraceMs ?? DEFAULT_ORPHAN_GRACE_MS,
maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
});
resultCacheDiagnostics.cacheBytes = result.remainingBytes;
resultCacheDiagnostics.evictions += result.deletedEntries;
resultCacheDiagnostics.orphanDeletions += result.orphanDeletions;
return result;
}
export async function deleteTabResultSnapshotsForOwner(ownerId: string): Promise<void> {
await Promise.all(availableResultCacheBackends(true).map((backend) => backend.deleteOwner(ownerId)));
}
@@ -51,6 +51,7 @@ import { findDatabaseTreeNode } from "@/lib/sidebar/treeRefreshTarget";
import { shouldMarkDisconnected } from "@/lib/connection/connectionHealth";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { requiresSqlServerLegacyCompatibilityComponent, SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY } from "@/lib/connection/sqlServerLegacyCompatibility";
import { deleteTabResultSnapshotsForOwner } from "@/lib/tabs/tabResultCache";
import { connectionUsesVisibleSchemaFilter, filterDatabaseNamesForConnection, filterSchemaNamesForConnection, filterVisibleDatabaseNames, normalizeVisibleDatabaseSelection } from "@/lib/database/visibleDatabases";
import {
buildObjectGroupPlaceholderNodes,
@@ -1756,6 +1757,7 @@ export const useConnectionStore = defineStore("connection", () => {
for (const id of removedIds) {
invalidateCompletionCache(id);
clearLoadedChildrenCache(id);
void deleteTabResultSnapshotsForOwner(id);
}
}
+128 -82
View File
@@ -44,7 +44,8 @@ import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
import { buildTabResultSnapshot, deleteTabResultSnapshot, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
import { queryResultBaseSql, queryResultExecutionSql } from "@/lib/tabs/tabPresentation";
import { isMysqlExecutionErrorResult } from "@/lib/query/queryResultError";
import { decodeQueryResultArchive, encodeQueryResultArchive, type DecodedQueryResultArchive } from "@/lib/query/queryResultArchive";
@@ -52,6 +53,8 @@ import * as api from "@/lib/backend/api";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { recordQueryCancellationLatency, resourceLifecycleDiagnostics } from "@/lib/diagnostics/resourceLifecycleDiagnostics";
import { appendDebugLog } from "@/lib/backend/debugLog";
import { createSavedSqlEditorPosition, initSavedSqlEditorPositions, restoreSavedSqlEditorPosition, saveSavedSqlEditorPosition } from "@/lib/app/savedSqlEditorPosition";
import { ensureSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { safeLocalStorageGet, safeLocalStorageRemove } from "@/lib/backend/safeStorage";
@@ -416,6 +419,11 @@ export const useQueryStore = defineStore("query", () => {
return tableStructureRefreshVersions.value[tableStructureKey(connectionId, database, schema, tableName)] ?? 0;
}
const MAX_CACHED_RESULTS = 5;
const MAX_CACHED_RESULT_BYTES = 128 * 1024 * 1024;
function queryExecutionLog(level: "debug" | "info" | "warn" | "error", event: string, details: Record<string, unknown>) {
appendDebugLog(level, `[DBX][executeTabSql:${event}]`, details);
}
async function closeResultSession(tab: QueryTab | undefined, preserveSessionId?: string, throwOnError = false) {
const sessionId = tab?.resultSessionId ?? tab?.result?.session_id;
@@ -481,6 +489,7 @@ export const useQueryStore = defineStore("query", () => {
function touchResult(tab: QueryTab | undefined, accessedAt = Date.now()) {
if (tab?.result || tab?.results) {
tab.resultAccessedAt = accessedAt;
tab.resultEstimatedBytes = estimateQueryResultsBytes(tab.result, tab.results);
tab.resultCacheState = "memory";
tab.resultEvicted = undefined;
}
@@ -496,6 +505,7 @@ export const useQueryStore = defineStore("query", () => {
tab.resultSortMode = undefined;
tab.resultSessionId = undefined;
tab.resultAccessedAt = undefined;
tab.resultEstimatedBytes = undefined;
tab.queryAnalysis = undefined;
tab.querySourceColumns = undefined;
tab.queryEditabilityReason = undefined;
@@ -521,6 +531,7 @@ export const useQueryStore = defineStore("query", () => {
run.resultLocalSortOriginalRows = undefined;
run.resultLocalSortOriginalMongoDocuments = undefined;
run.resultSessionId = undefined;
run.resultEstimatedBytes = undefined;
run.queryAnalysis = undefined;
run.querySourceColumns = undefined;
run.queryEditabilityReason = undefined;
@@ -556,6 +567,7 @@ export const useQueryStore = defineStore("query", () => {
tab.resultAccessedAt = run.resultAccessedAt;
tab.resultCacheKey = run.resultCacheKey;
tab.resultCacheState = run.resultCacheState;
tab.resultEstimatedBytes = run.resultEstimatedBytes ?? estimateQueryResultsBytes(run.result, run.results);
tab.resultEvicted = run.resultEvicted;
tab.queryAnalysis = run.queryAnalysis;
tab.querySourceColumns = run.querySourceColumns;
@@ -632,24 +644,28 @@ export const useQueryStore = defineStore("query", () => {
const key = run.resultCacheKey ?? resultRunCacheKey(tab.id, run.id);
run.resultCacheKey = key;
run.resultCacheState = "memory";
return writeTabResultSnapshot(key, {
result: run.result,
results: run.results,
activeResultIndex: run.activeResultIndex,
resultEditorFingerprint: run.resultEditorFingerprint,
resultRuns: [run],
activeResultRunId: run.id,
queryAnalysis: run.queryAnalysis,
querySourceColumns: run.querySourceColumns,
queryEditabilityReason: run.queryEditabilityReason,
tableMeta: run.tableMeta,
resultPageSql: run.resultPageSql,
resultPageLimit: run.resultPageLimit,
resultPageOffset: run.resultPageOffset,
resultCountSql: run.resultCountSql,
resultTotalRowCount: run.resultTotalRowCount,
cachedAt: Date.now(),
});
return writeTabResultSnapshot(
key,
{
result: run.result,
results: run.results,
activeResultIndex: run.activeResultIndex,
resultEditorFingerprint: run.resultEditorFingerprint,
resultRuns: [run],
activeResultRunId: run.id,
queryAnalysis: run.queryAnalysis,
querySourceColumns: run.querySourceColumns,
queryEditabilityReason: run.queryEditabilityReason,
tableMeta: run.tableMeta,
resultPageSql: run.resultPageSql,
resultPageLimit: run.resultPageLimit,
resultPageOffset: run.resultPageOffset,
resultCountSql: run.resultCountSql,
resultTotalRowCount: run.resultTotalRowCount,
cachedAt: Date.now(),
},
tab.connectionId,
);
}
function evictInactiveResultRunPayloads(tab: QueryTab) {
@@ -698,6 +714,7 @@ export const useQueryStore = defineStore("query", () => {
resultTotalRowCountLoading: tab.resultTotalRowCountLoading,
resultSessionId: tab.resultSessionId,
resultAccessedAt: tab.resultAccessedAt,
resultEstimatedBytes: tab.resultEstimatedBytes,
resultCacheKey: tab.resultCacheKey,
resultCacheState: tab.resultCacheState,
resultEvicted: tab.resultEvicted,
@@ -750,6 +767,7 @@ export const useQueryStore = defineStore("query", () => {
resultTotalRowCountLoading: tab.resultTotalRowCountLoading,
resultSessionId: tab.resultSessionId,
resultAccessedAt: tab.resultAccessedAt,
resultEstimatedBytes: tab.resultEstimatedBytes,
resultCacheKey: tab.resultCacheKey,
resultCacheState: tab.resultCacheState,
resultEvicted: tab.resultEvicted,
@@ -823,7 +841,7 @@ export const useQueryStore = defineStore("query", () => {
async function evictCachedResult(tab: QueryTab) {
await closeResultSession(tab);
const cacheKey = tabResultCacheKey(tab.id);
const cached = await writeTabResultSnapshot(cacheKey, buildTabResultSnapshot(tab));
const cached = await writeTabResultSnapshot(cacheKey, buildTabResultSnapshot(tab), tab.connectionId);
tab.resultCacheKey = cached ? cacheKey : undefined;
tab.resultCacheState = cached ? "disk" : "missing";
clearResultPayload(tab, { evicted: true });
@@ -838,6 +856,16 @@ export const useQueryStore = defineStore("query", () => {
}
}
function scheduleResultCacheMaintenance() {
const maintain = () => {
const liveKeys = tabs.value.flatMap((tab) => [tab.resultCacheKey, ...(tab.resultRuns?.map((run) => run.resultCacheKey) ?? [])]).filter((key): key is string => !!key);
void pruneTabResultSnapshots(liveKeys).catch((error) => console.warn("[DBX][result-cache:maintenance:error]", error));
};
if (typeof requestIdleCallback !== "undefined") requestIdleCallback(maintain, { timeout: 5000 });
else if (typeof window !== "undefined") window.setTimeout(maintain, 0);
else setTimeout(maintain, 0);
}
async function initOpenTabs() {
if (isOpenTabsLoaded.value) return;
const saved = await api.loadOpenTabsState().catch(() => null);
@@ -851,6 +879,7 @@ export const useQueryStore = defineStore("query", () => {
await saveTabs(tabs.value, activeTabId.value).catch(() => undefined);
}
isOpenTabsLoaded.value = true;
scheduleResultCacheMaintenance();
return;
}
@@ -863,6 +892,7 @@ export const useQueryStore = defineStore("query", () => {
// would resurrect old tabs if the user later changes the setting.
clearLegacySavedTabs();
isOpenTabsLoaded.value = true;
scheduleResultCacheMaintenance();
return;
}
try {
@@ -875,6 +905,7 @@ export const useQueryStore = defineStore("query", () => {
}
}
isOpenTabsLoaded.value = true;
scheduleResultCacheMaintenance();
}
const _persistSnapshot = computed(() =>
@@ -1452,6 +1483,7 @@ export const useQueryStore = defineStore("query", () => {
void closeResultSession(tabs.value[idx]);
void closeClientConnectionSession(tabs.value[idx]);
clearResultRunSnapshots(tabs.value[idx]);
void deleteTabResultSnapshot(tabResultCacheKey(id));
clearResultPayload(tabs.value[idx]);
tabs.value.splice(idx, 1);
if (tab.externalSqlPath) refreshExternalSqlFileTitles();
@@ -1703,6 +1735,7 @@ export const useQueryStore = defineStore("query", () => {
void closeResultSession(tab);
void closeClientConnectionSession(tab);
clearResultRunSnapshots(tab);
void deleteTabResultSnapshot(tabResultCacheKey(tab.id));
clearResultPayload(tab);
});
@@ -2265,7 +2298,7 @@ export const useQueryStore = defineStore("query", () => {
tableName: metadataTableName,
};
const knownTableType = tab.tableMeta?.tableName.toLowerCase() === metadataTableName.toLowerCase() && normalizeOptionalSchema(tab.tableMeta.schema) === normalizeOptionalSchema(metadataSchema) ? tab.tableMeta.tableType : undefined;
console.info("[DBX][executeTabSql:metadata:table:start]", {
queryExecutionLog("info", "metadata:table:start", {
traceId,
schema: metadataSchema,
table: metadataTableName,
@@ -2281,11 +2314,11 @@ export const useQueryStore = defineStore("query", () => {
databaseType: dbType,
driverProfile: conn?.driver_profile || conn?.db_type,
catalog: metadataCatalog,
traceLogger: (event) => console.debug("[DBX][executeTabSql:metadata:table-trace]", { sourceTraceId: traceId, ...event }),
traceLogger: (event) => queryExecutionLog("debug", "metadata:table-trace", { sourceTraceId: traceId, ...event }),
});
const columns = loadedMetadata.metadata.columns;
const primaryKeys = loadedMetadata.metadata.primaryKeys;
console.info("[DBX][executeTabSql:metadata:table:done]", {
queryExecutionLog("info", "metadata:table:done", {
traceId,
columnCount: columns.length,
primaryKeyCount: primaryKeys.length,
@@ -2356,7 +2389,7 @@ export const useQueryStore = defineStore("query", () => {
sourceExpressions: databaseType === "oracle" && missingPrimaryKeys.includes(DBX_ROWID_COLUMN) ? { [DBX_ROWID_COLUMN]: "ROWIDTOCHAR(ROWID)" } : undefined,
});
if (!rewritten) return unchanged;
console.info("[DBX][executeTabSql:hidden-primary-keys]", {
queryExecutionLog("info", "hidden-primary-keys", {
traceId,
table: loaded.tableMeta.tableName,
keyCount: rewritten.projections.length,
@@ -2366,7 +2399,7 @@ export const useQueryStore = defineStore("query", () => {
} catch (error) {
// Metadata enrichment is optional. Query execution must retain its prior
// behavior when metadata is unavailable or the SQL cannot be rewritten.
console.warn("[DBX][executeTabSql:hidden-primary-keys:skip]", { traceId, error, elapsed: elapsed() });
queryExecutionLog("warn", "hidden-primary-keys:skip", { traceId, error, elapsed: elapsed() });
return unchanged;
}
}
@@ -2382,9 +2415,9 @@ export const useQueryStore = defineStore("query", () => {
};
}
console.info("[DBX][executeTabSql:metadata:editability:start]", { traceId, elapsed: elapsed?.() });
queryExecutionLog("info", "metadata:editability:start", { traceId, elapsed: elapsed?.() });
const editability = await api.analyzeEditableQueryEditability(sql);
console.info("[DBX][executeTabSql:metadata:editability:done]", {
queryExecutionLog("info", "metadata:editability:done", {
traceId,
editable: editability.editable,
reason: editability.editable ? undefined : editability.reason,
@@ -2533,7 +2566,7 @@ export const useQueryStore = defineStore("query", () => {
void (async () => {
const tab = tabs.value.find((t) => t.id === tabId);
if (!tab || tab.result !== result) return;
console.info("[DBX][executeTabSql:metadata:start]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "metadata:start", { traceId, elapsed: elapsed() });
const patch = await buildQueryMetadataPatch(tab, sql, traceId, elapsed, hiddenPrimaryKeys);
if (patch?.queryAnalysis && hiddenPrimaryKeys.length > 0) {
patch.queryAnalysis = { ...patch.queryAnalysis, allowInsert: false };
@@ -2542,9 +2575,9 @@ export const useQueryStore = defineStore("query", () => {
if (patch && current?.result === result) {
applyQueryMetadataPatch(current, patch);
syncActiveResultRunFromDisplayed(current);
console.info("[DBX][executeTabSql:metadata:done]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "metadata:done", { traceId, elapsed: elapsed() });
} else {
console.warn("[DBX][executeTabSql:metadata:stale]", { traceId, elapsed: elapsed() });
queryExecutionLog("warn", "metadata:stale", { traceId, elapsed: elapsed() });
}
})();
}
@@ -2604,7 +2637,7 @@ export const useQueryStore = defineStore("query", () => {
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined);
return;
}
console.info("[DBX][executeTabSql:count:start]", { traceId: options.traceId, elapsed: options.elapsed() });
queryExecutionLog("info", "count:start", { traceId: options.traceId, elapsed: options.elapsed() });
const countResult = await api.executeQuery(options.connectionId, options.database, countTarget.sql, countTarget.schema, countExecutionId, {
clientSessionId,
timeoutSecs: options.timeoutSecs,
@@ -2615,14 +2648,14 @@ export const useQueryStore = defineStore("query", () => {
return;
}
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, total);
console.info("[DBX][executeTabSql:count:done]", {
queryExecutionLog("info", "count:done", {
traceId: options.traceId,
total,
elapsed: options.elapsed(),
});
} catch (error) {
setQueryTotalRowCountIfCurrent(options.tabId, options.executionId, options.result, undefined);
console.warn("[DBX][executeTabSql:count:error]", {
queryExecutionLog("warn", "count:error", {
traceId: options.traceId,
elapsed: options.elapsed(),
error,
@@ -2686,15 +2719,12 @@ export const useQueryStore = defineStore("query", () => {
if (!options?.preserveResultDuringExecution || !tab.result) {
clearResultPayload(tab);
}
console.info("[DBX][executeTabSql:start]", {
queryExecutionLog("info", "start", {
traceId,
tabId: id,
mode: tab.mode,
connectionId: tab.connectionId,
database: tab.database,
schema: tab.schema,
sourceTraceId: options?.sourceTraceId,
sql,
sqlLength: sql.length,
});
const queryBaseSql = options?.resultBaseSql ?? sql;
let sqlToExecute = sql;
@@ -2715,13 +2745,13 @@ export const useQueryStore = defineStore("query", () => {
const mongoNeedsConnection = mongoCommands.some(({ command }) => command.kind !== "use");
if (options?.skipEnsureConnected) {
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "caller" });
queryExecutionLog("info", "ensure-connected:skip", { traceId, elapsed: elapsed(), reason: "caller" });
} else if (conn?.db_type === "mongodb" && mongoCommands.length > 0 && !mongoNeedsConnection) {
console.info("[DBX][executeTabSql:ensure-connected:skip]", { traceId, elapsed: elapsed(), reason: "mongo-use-only" });
queryExecutionLog("info", "ensure-connected:skip", { traceId, elapsed: elapsed(), reason: "mongo-use-only" });
} else {
console.info("[DBX][executeTabSql:ensure-connected:start]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "ensure-connected:start", { traceId, elapsed: elapsed() });
await connStore.ensureConnected(tab.connectionId);
console.info("[DBX][executeTabSql:ensure-connected:done]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "ensure-connected:done", { traceId, elapsed: elapsed() });
}
conn = connStore.getConfig(tab.connectionId);
if (parsedMongoCommands === undefined && conn?.db_type === "mongodb") {
@@ -2732,9 +2762,9 @@ export const useQueryStore = defineStore("query", () => {
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const settingsStore = useSettingsStore();
console.info("[DBX][executeTabSql:previous-session-close:start]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "previous-session-close:start", { traceId, elapsed: elapsed() });
await previousResultSessionClose;
console.info("[DBX][executeTabSql:previous-session-close:done]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "previous-session-close:done", { traceId, elapsed: elapsed() });
// Redis command execution — split multi-line input into individual commands
if (conn?.db_type === "redis") {
@@ -2745,7 +2775,7 @@ export const useQueryStore = defineStore("query", () => {
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (commands.length === 0) return;
console.info("[DBX][executeTabSql:redis:start]", { traceId, db: currentDb, commandCount: commands.length, sql });
queryExecutionLog("info", "redis:start", { traceId, db: currentDb, commandCount: commands.length, sqlLength: sql.length });
const allResults: QueryResult[] = [];
const commandRanges = executableStatementRanges(sql, "redis");
@@ -2769,7 +2799,7 @@ export const useQueryStore = defineStore("query", () => {
allResults.push(annotateQueryResultSource({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }, command, undefined, undefined, sourceRange));
}
}
console.info("[DBX][executeTabSql:redis:done]", { traceId, commandCount: commands.length, elapsed: elapsed() });
queryExecutionLog("info", "redis:done", { traceId, commandCount: commands.length, elapsed: elapsed() });
const current = tabs.value.find((t) => t.id === id);
if (current?.executionId === executionId) {
@@ -2809,7 +2839,7 @@ export const useQueryStore = defineStore("query", () => {
}
if (mongoCommands.length > 0) {
console.info("[DBX][executeTabSql:mongo:start]", { traceId, database: tab.database, commandCount: mongoCommands.length, sql });
queryExecutionLog("info", "mongo:start", { traceId, commandCount: mongoCommands.length, sqlLength: sql.length });
const allResults: QueryResult[] = [];
// Track the effective db as we walk the batch so later commands observe
@@ -2832,12 +2862,12 @@ export const useQueryStore = defineStore("query", () => {
try {
switch (mongoCommand.kind) {
case "find": {
console.info("[DBX][executeTabSql:mongo-find:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
queryExecutionLog("info", "mongo-find:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.skip, mongoCommand.limit, mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, executionId);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total)));
allResults.push(queryResult);
mongoEditTarget = mongoCommands.length === 1 && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
console.info("[DBX][executeTabSql:mongo-find:done]", {
queryExecutionLog("info", "mongo-find:done", {
traceId,
collection: mongoCommand.collection,
database: currentDatabase,
@@ -2848,12 +2878,12 @@ export const useQueryStore = defineStore("query", () => {
break;
}
case "findOne": {
console.info("[DBX][executeTabSql:mongo-find-one:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
queryExecutionLog("info", "mongo-find-one:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const result = await api.mongoFindOne(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total)));
allResults.push(queryResult);
mongoEditTarget = mongoCommands.length === 1 && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
console.info("[DBX][executeTabSql:mongo-find-one:done]", {
queryExecutionLog("info", "mongo-find-one:done", {
traceId,
collection: mongoCommand.collection,
database: currentDatabase,
@@ -2863,11 +2893,11 @@ export const useQueryStore = defineStore("query", () => {
break;
}
case "version": {
console.info("[DBX][executeTabSql:mongo-version:start]", { traceId, database: currentDatabase });
queryExecutionLog("info", "mongo-version:start", { traceId, database: currentDatabase });
const version = await api.mongoServerVersion(tab.connectionId, currentDatabase, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoVersionToQueryResult(version, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-version:done]", {
queryExecutionLog("info", "mongo-version:done", {
traceId,
database: currentDatabase,
version,
@@ -2876,11 +2906,11 @@ export const useQueryStore = defineStore("query", () => {
break;
}
case "countDocuments": {
console.info("[DBX][executeTabSql:mongo-count:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
queryExecutionLog("info", "mongo-count:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const total = await api.mongoCountDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.mode, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCountToQueryResult(total, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-count:done]", {
queryExecutionLog("info", "mongo-count:done", {
traceId,
collection: mongoCommand.collection,
database: currentDatabase,
@@ -2894,12 +2924,12 @@ export const useQueryStore = defineStore("query", () => {
const safety = evaluateMongoAggregateSafety(mongoCommand, options.mongoSafety);
if (!safety.allowed) throw new Error(safety.reason);
}
console.info("[DBX][executeTabSql:mongo-aggregate:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
queryExecutionLog("info", "mongo-aggregate:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize);
const result = await api.mongoAggregateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-aggregate:done]", {
queryExecutionLog("info", "mongo-aggregate:done", {
traceId,
collection: mongoCommand.collection,
database: currentDatabase,
@@ -2910,11 +2940,11 @@ export const useQueryStore = defineStore("query", () => {
break;
}
case "getIndexes": {
console.info("[DBX][executeTabSql:mongo-indexes:start]", { traceId, collection: mongoCommand.collection, database: currentDatabase });
queryExecutionLog("info", "mongo-indexes:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const indexes = await api.listIndexes(tab.connectionId, currentDatabase, "", mongoCommand.collection);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-indexes:done]", {
queryExecutionLog("info", "mongo-indexes:done", {
traceId,
collection: mongoCommand.collection,
database: currentDatabase,
@@ -2924,7 +2954,7 @@ export const useQueryStore = defineStore("query", () => {
break;
}
case "collectionStats": {
console.info("[DBX][executeTabSql:mongo-collection-stats:start]", {
queryExecutionLog("info", "mongo-collection-stats:start", {
traceId,
collection: mongoCommand.collection,
metric: mongoCommand.metric,
@@ -2933,7 +2963,7 @@ export const useQueryStore = defineStore("query", () => {
const stats = await api.mongoCollectionStats(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.scale, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCollectionStatsToQueryResult(mongoCommand.metric, stats as unknown as Record<string, unknown>, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-collection-stats:done]", {
queryExecutionLog("info", "mongo-collection-stats:done", {
traceId,
collection: mongoCommand.collection,
metric: mongoCommand.metric,
@@ -2949,7 +2979,7 @@ export const useQueryStore = defineStore("query", () => {
const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety);
if (!safety.allowed) throw new Error(safety.reason);
}
console.info("[DBX][executeTabSql:mongo-find-and-modify:start]", {
queryExecutionLog("info", "mongo-find-and-modify:start", {
traceId,
kind: mongoCommand.kind,
collection: mongoCommand.collection,
@@ -2963,7 +2993,7 @@ export const useQueryStore = defineStore("query", () => {
: await api.mongoFindOneAndDelete(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-find-and-modify:done]", {
queryExecutionLog("info", "mongo-find-and-modify:done", {
traceId,
kind: mongoCommand.kind,
collection: mongoCommand.collection,
@@ -2984,7 +3014,7 @@ export const useQueryStore = defineStore("query", () => {
const safety = evaluateMongoWriteSafety(mongoCommand, options.mongoSafety);
if (!safety.allowed) throw new Error(safety.reason);
}
console.info("[DBX][executeTabSql:mongo-write:start]", {
queryExecutionLog("info", "mongo-write:start", {
traceId,
database: currentDatabase,
kind: mongoCommand.kind,
@@ -3010,7 +3040,7 @@ export const useQueryStore = defineStore("query", () => {
const result = await api.mongoDeleteDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
}
console.info("[DBX][executeTabSql:mongo-write:done]", {
queryExecutionLog("info", "mongo-write:done", {
traceId,
database: currentDatabase,
kind: mongoCommand.kind,
@@ -3023,7 +3053,7 @@ export const useQueryStore = defineStore("query", () => {
currentDatabase = mongoCommand.database;
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoUseToQueryResult(currentDatabase, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
console.info("[DBX][executeTabSql:mongo-use:done]", {
queryExecutionLog("info", "mongo-use:done", {
traceId,
database: currentDatabase,
elapsed: elapsed(),
@@ -3039,7 +3069,7 @@ export const useQueryStore = defineStore("query", () => {
}
}
console.info("[DBX][executeTabSql:mongo:done]", {
queryExecutionLog("info", "mongo:done", {
traceId,
database: currentDatabase,
commandCount: mongoCommands.length,
@@ -3127,14 +3157,14 @@ export const useQueryStore = defineStore("query", () => {
let executionPromise: Promise<QueryResult[]>;
if (tab.autoCommit === false) {
if (!tab.txnSessionId) {
console.info("[DBX][executeTabSql:begin-manual-txn:start]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "begin-manual-txn:start", { traceId, elapsed: elapsed() });
tab.txnSessionId = await api.beginManualTransaction(tab.connectionId, executionDatabase, executionSchema);
console.info("[DBX][executeTabSql:begin-manual-txn:done]", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
queryExecutionLog("info", "begin-manual-txn:done", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
}
console.info("[DBX][executeTabSql:execute-in-txn:invoke]", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit);
} else {
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "execute-multi:start", { traceId, elapsed: elapsed() });
// Query and data tabs use a tab-scoped pool so repeated executions keep
// connection-local state and avoid MySQL pool resets on every refresh.
const clientSessionId = tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined;
@@ -3152,7 +3182,7 @@ export const useQueryStore = defineStore("query", () => {
timeoutSecs: queryTimeoutSecs,
continueOnError: settingsStore.editorSettings.continueOnErrorOnBatch,
};
console.info("[DBX][executeTabSql:execute-multi:invoke]", {
queryExecutionLog("info", "execute-multi:invoke", {
traceId,
elapsed: elapsed(),
executionSchema,
@@ -3169,7 +3199,7 @@ export const useQueryStore = defineStore("query", () => {
} else if (hiddenPrimaryKeys.length > 0) {
queryMetadataSql = queryBaseSql;
}
console.info("[DBX][executeTabSql:execute-multi:done]", {
queryExecutionLog("info", "execute-multi:done", {
traceId,
resultCount: results.length,
rowCounts: results.map((result) => result.rows.length),
@@ -3262,7 +3292,7 @@ export const useQueryStore = defineStore("query", () => {
timeoutSecs: queryTimeoutSecs,
});
}
console.info("[DBX][executeTabSql:result:assigned]", {
queryExecutionLog("info", "result:assigned", {
traceId,
activeResultIndex: current.activeResultIndex,
rowCount: current.result?.rows.length ?? 0,
@@ -3274,14 +3304,14 @@ export const useQueryStore = defineStore("query", () => {
analyzeQueryMetadataInBackground(id, displayedQueryMetadataSql(current, queryMetadataSql), current.result, traceId, elapsed, hiddenPrimaryKeys);
}
} else {
console.warn("[DBX][executeTabSql:stale-result]", {
queryExecutionLog("warn", "stale-result", {
traceId,
currentExecutionId: current?.executionId,
elapsed: elapsed(),
});
}
} catch (e: any) {
console.error("[DBX][executeTabSql:error]", { traceId, elapsed: elapsed(), error: e });
queryExecutionLog("error", "error", { traceId, elapsed: elapsed(), error: e });
// Sync connection state if the error indicates a lost connection
useConnectionStore().recordConnectionLostError(tab.connectionId, e);
// Handle manual transaction auto-rollback (e.g. deadlock detected by server,
@@ -3332,9 +3362,9 @@ export const useQueryStore = defineStore("query", () => {
current.isCancelling = false;
current.queryExecutionStartedAt = undefined;
current.executionId = undefined;
console.info("[DBX][executeTabSql:finish]", { traceId, elapsed: elapsed() });
queryExecutionLog("info", "finish", { traceId, elapsed: elapsed() });
} else {
console.warn("[DBX][executeTabSql:finish-stale]", {
queryExecutionLog("warn", "finish-stale", {
traceId,
currentExecutionId: current?.executionId,
elapsed: elapsed(),
@@ -3561,6 +3591,7 @@ export const useQueryStore = defineStore("query", () => {
const executionId = tab.executionId;
if (!executionId) return false;
tab.isCancelling = true;
const cancellationStartedAt = performance.now();
try {
const canceled = await withCancelQueryTimeout(api.cancelQuery(executionId));
if (canceled) {
@@ -3588,6 +3619,8 @@ export const useQueryStore = defineStore("query", () => {
current.result = toErrorResult(e);
}
return false;
} finally {
recordQueryCancellationLatency(performance.now() - cancellationStartedAt);
}
}
@@ -3648,9 +3681,20 @@ export const useQueryStore = defineStore("query", () => {
}
async function trimResultCache() {
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results)).sort((a, b) => (a.resultAccessedAt ?? 0) - (b.resultAccessedAt ?? 0));
if (inactive.length > MAX_CACHED_RESULTS) {
const toEvict = inactive.slice(0, inactive.length - MAX_CACHED_RESULTS);
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results));
const evictionIds = new Set(
selectInactiveResultEvictions(
inactive.map((tab) => ({
id: tab.id,
estimatedBytes: tab.resultEstimatedBytes ?? estimateQueryResultsBytes(tab.result, tab.results),
accessedAt: tab.resultAccessedAt ?? 0,
})),
MAX_CACHED_RESULT_BYTES,
MAX_CACHED_RESULTS,
),
);
const toEvict = inactive.filter((tab) => evictionIds.has(tab.id));
if (toEvict.length > 0) {
await Promise.all(toEvict.map((t) => evictCachedResult(t)));
}
}
@@ -3808,13 +3852,14 @@ export const useQueryStore = defineStore("query", () => {
return openResultArchiveTab(archive);
}
async function reloadEvictedTab(id: string) {
async function reloadEvictedTab(id: string, { reexecuteOnMissing = false }: { reexecuteOnMissing?: boolean } = {}) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || !tab.resultEvicted) return;
if (tab.resultCacheKey) {
const restored = restoreCachedResultPayload(tab, await readTabResultSnapshot(tab.resultCacheKey));
if (restored) return;
tab.resultCacheState = "missing";
if (!reexecuteOnMissing) return;
}
tab.resultEvicted = false;
const sql = tab.lastExecutedSql ?? tab.sql;
@@ -4120,6 +4165,7 @@ export const useQueryStore = defineStore("query", () => {
importResultArchive,
fetchTabResultForExport,
buildQueryResultExportRequest,
getResourceLifecycleDiagnostics: () => resourceLifecycleDiagnostics(tabs.value),
notifyConnectionMayBeLost,
};
});
+2
View File
@@ -557,6 +557,7 @@ export interface QueryResultRun {
resultTotalRowCountLoading?: boolean;
resultSessionId?: string;
resultAccessedAt?: number;
resultEstimatedBytes?: number;
resultCacheKey?: string;
resultCacheState?: "memory" | "disk" | "missing";
resultEvicted?: boolean;
@@ -780,6 +781,7 @@ export interface QueryTab {
resultTotalRowCountLoading?: boolean;
resultSessionId?: string;
resultAccessedAt?: number;
resultEstimatedBytes?: number;
resultCacheKey?: string;
resultCacheState?: "memory" | "disk" | "missing";
pinned?: boolean;