feat(query): cross-database timing details, drop OceanBase audit query

This commit is contained in:
efeire authored and GitHub committed 2026-09-24 19:17:27 +08:00
1 parent 4b9a1c4d5c
commit d0497e0ca6
69 files changed
+973 -180

No files matched your search

@@ -1,4 +1,5 @@
<script setup lang="ts">
import QueryTimingDetails from "./QueryTimingDetails.vue";
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import { applyDdlSearchMarks } from "@/lib/sql/ddlSearchMarks";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
@@ -6713,7 +6714,7 @@ let dataGridIsActive = true;
let canvasRuntime: DataGridCanvasRuntime;
const { elapsedMs: resultViewUpdateMs, canvasDrawCompleted: completeResultCanvasDraw } = useResultViewUpdateTiming(
() => props.result,
() => resolvedDatabaseType.value === "oceanbase-oracle" && isResultsContext.value,
() => isResultsContext.value,
() => (useCanvasGridRows.value ? "canvas" : "dom"),
);
@@ -13987,12 +13988,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
</span>
<span v-if="showTruncationWarning" class="shrink-0 text-amber-500 text-xs">(truncated)</span>
<span v-if="!hasData" class="shrink-0">{{ t("grid.rowsAffected", { count: result.affected_rows }) }}</span>
<template v-if="resolvedDatabaseType === 'oceanbase-oracle' && isResultsContext">
<span class="shrink-0" :title="t('grid.serverExecuteTimeHint')">{{ result.server_execute_time_us !== undefined ? t("grid.serverExecuteTime", { us: result.server_execute_time_us }) : t("grid.serverExecuteTimeUnavailable") }}</span>
<span class="shrink-0" :title="t('grid.agentExecuteTimeHint')">{{ t("grid.agentExecuteTime", { ms: result.execution_time_ms }) }}</span>
<span v-if="result.client_request_wait_ms !== undefined" class="shrink-0" :title="t('grid.clientRequestWaitHint')">{{ t("grid.clientRequestWait", { ms: result.client_request_wait_ms }) }}</span>
<span v-if="resultViewUpdateMs !== undefined" class="shrink-0" :title="t('grid.resultViewUpdateHint')">{{ t("grid.resultViewUpdate", { ms: resultViewUpdateMs }) }}</span>
</template>
<QueryTimingDetails v-if="isResultsContext" :result="result" :render-ms="resultViewUpdateMs" />
<span v-else class="shrink-0">{{ result.execution_time_ms }}ms</span>
<template v-if="editable && hasDataGridSaveTarget">
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { QueryResult } from "@/types/database";
const props = defineProps<{ result: QueryResult; renderMs?: number }>();
const { t } = useI18n();
const measured = (value: number | undefined): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
const format = (value: number | undefined) => (measured(value) ? `${Number(value.toFixed(3))}ms` : t("grid.timingUnavailable"));
const wait = computed(() => (measured(props.result.client_request_wait_ms) ? `${Math.round(props.result.client_request_wait_ms)}ms` : "—"));
const hasAgentTiming = computed(() => measured(props.result.query_timings_ms?.agent_total));
const phases = computed(() => {
const m = props.result.query_timings_ms ?? {};
const waitMs = props.result.client_request_wait_ms;
// This is a cross-boundary remainder, not a measured network duration.
const remainder = measured(waitMs) && measured(m.agent_total) && measured(m.core_lock) ? Math.max(0, waitMs - m.agent_total - m.core_lock) : undefined;
const agentOther = measured(m.agent_total)
? Math.max(
0,
m.agent_total -
Object.entries(m)
.filter(([key]) => !["agent_total", "core_lock"].includes(key))
.reduce((sum, [, value]) => sum + (measured(value) ? value : 0), 0),
)
: undefined;
const stages = [
{ key: "prepare", value: props.result.client_prepare_ms },
{ key: "request", value: waitMs },
{ key: "core_lock", value: m.core_lock },
{ key: "pool_acquire", value: m.pool_acquire },
{ key: "session_prepare", value: m.session_prepare },
{ key: "schema", value: m.schema },
{ key: "statement_prepare", value: m.statement_prepare },
{ key: "jdbc_execute", value: m.jdbc_execute },
{ key: "metadata", value: m.metadata },
{ key: "fetch", value: m.fetch },
{ key: "pool_release", value: m.pool_release },
{ key: "agent_other", value: agentOther },
{ key: "remainder", value: remainder },
{ key: "result", value: props.result.client_result_ms },
{ key: "render", value: props.renderMs },
];
return stages.filter((stage) => {
if (["prepare", "result", "render"].includes(stage.key)) return true;
if (stage.key === "request") return !hasAgentTiming.value;
return hasAgentTiming.value && measured(stage.value);
});
});
</script>
<template>
<TooltipProvider>
<Tooltip :delay-duration="200">
<TooltipTrigger as-child>
<button type="button" class="shrink-0 cursor-help rounded-sm tabular-nums focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" :aria-label="t('grid.timingDetails') + ': ' + wait" data-testid="query-timing-trigger">{{ wait }}</button>
</TooltipTrigger>
<TooltipContent side="top" align="start" class="block max-w-sm max-h-[var(--reka-tooltip-content-available-height)] overflow-y-auto">
<div class="space-y-2" data-testid="query-timing-details">
<p class="font-medium">{{ t("grid.clientRequestWait", { ms: measured(result.client_request_wait_ms) ? Number(result.client_request_wait_ms.toFixed(3)) : t("grid.timingUnavailable") }) }}</p>
<p v-if="(result.timing_page_count ?? 1) > 1">{{ t("grid.timingPages", { count: result.timing_page_count }) }}</p>
<table class="w-full border-collapse text-left" :aria-label="t('grid.timingDetails')">
<thead class="border-b border-current/20">
<tr>
<th scope="col" class="pb-1 pr-3 font-medium">{{ t("grid.timingNumber") }}</th>
<th scope="col" class="pb-1 pr-4 font-medium">{{ t("grid.timingStage") }}</th>
<th scope="col" class="pb-1 text-right font-medium">{{ t("grid.timingDuration") }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(phase, index) in phases" :key="phase.key">
<td class="py-0.5 pr-3 tabular-nums">{{ index + 1 }}.</td>
<th scope="row" class="py-0.5 pr-4 font-normal">{{ t(`grid.timingPhase_${phase.key}`) }}</th>
<td class="whitespace-nowrap py-0.5 text-right tabular-nums">{{ format(phase.value) }}</td>
</tr>
</tbody>
</table>
<dl v-if="hasAgentTiming" class="border-t border-current/20 pt-2">
<div class="flex justify-between gap-4">
<dt>{{ t("grid.timingBackend") }}</dt>
<dd class="tabular-nums">{{ format(result.execution_time_ms) }}</dd>
</div>
</dl>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
@@ -0,0 +1,82 @@
// @vitest-environment happy-dom
import { createApp, nextTick } from "vue";
import { createI18n } from "vue-i18n";
import { describe, expect, it } from "vitest";
import QueryTimingDetails from "../QueryTimingDetails.vue";
import zh from "@/i18n/locales/zh-CN";
describe("query timing disclosure", () => {
it("shows only wait, then ordered details on keyboard focus", async () => {
const host = document.createElement("div");
document.body.append(host);
const app = createApp(QueryTimingDetails, {
result: {
columns: [],
rows: [],
affected_rows: 0,
execution_time_ms: 55,
client_request_wait_ms: 7030,
server_execute_time_us: 1019,
query_timings_ms: { pool_acquire: 350, pool_release: 5, session_prepare: 2, schema: 3, statement_prepare: 1, jdbc_execute: 40, metadata: 2, fetch: 7, trace: 1, audit: 6000, agent_total: 6411 },
},
renderMs: 4,
});
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
app.mount(host);
await nextTick();
expect(host.textContent).toBe("7030ms");
expect(host.textContent).not.toContain("数据库执行");
host.querySelector<HTMLButtonElement>("button")!.focus();
await new Promise((resolve) => setTimeout(resolve, 100));
await nextTick();
const text = document.body.textContent!;
expect(text).toContain("1.");
expect(text).not.toContain("读取审计记录");
expect(text).not.toContain("读取追踪 ID");
expect(text).not.toContain("数据库计划");
expect(text).toContain("后端报告耗时");
expect(text).not.toContain("驱动报告耗时");
expect(text).toContain("获取连接");
expect(text).toContain("归还连接");
const details = document.querySelector('[data-testid="query-timing-details"]')!;
const stages = [...details.querySelectorAll("tbody tr")].map((row) => row.textContent!);
// No Core lock sample means both the lock and dependent remainder are absent.
expect(stages).toHaveLength(12);
expect([...details.querySelectorAll("thead th")].map((cell) => cell.textContent)).toEqual(["序号", "阶段", "耗时"]);
expect(stages.findIndex((s) => s.includes("获取连接"))).toBeLessThan(stages.findIndex((s) => s.includes("会话")));
expect(stages.findIndex((s) => s.includes("归还连接"))).toBeGreaterThan(stages.findIndex((s) => s.includes("读取结果")));
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
app.unmount();
host.remove();
});
it("shows common request stages without inventing JDBC steps for a native or HTTP result", async () => {
const host = document.createElement("div");
document.body.append(host);
const app = createApp(QueryTimingDetails, { result: { columns: [], rows: [], affected_rows: 0, execution_time_ms: 4, client_request_wait_ms: 12, client_prepare_ms: 2, client_result_ms: 1 }, renderMs: 3 });
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
app.mount(host);
await nextTick();
host.querySelector<HTMLButtonElement>("button")!.focus();
await new Promise((resolve) => setTimeout(resolve, 100));
await nextTick();
const details = document.querySelector('[data-testid="query-timing-details"]')!;
expect(details.querySelectorAll("tbody tr")).toHaveLength(4);
expect(details.textContent).toContain("请求等待");
expect(details.textContent).not.toContain("JDBC");
expect(details.textContent).not.toContain("获取连接");
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
app.unmount();
host.remove();
});
it("does not substitute execution time when wait or phases are missing", async () => {
const host = document.createElement("div");
document.body.append(host);
const app = createApp(QueryTimingDetails, { result: { columns: [], rows: [], affected_rows: 0, execution_time_ms: 55 } });
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
app.mount(host);
await nextTick();
expect(host.textContent).toBe("—");
app.unmount();
host.remove();
});
});
+23 -1
View File
@@ -2094,8 +2094,30 @@ export default {
serverExecuteTimeHint:
"OceanBase SQL Audit EXECUTE_TIME measures server plan execution. A JDBC cursor shows its completed statement sample; independent page queries show the sum only when every page is sampled. This overlaps with execution and wait timings. Unavailable while a cursor remains open or when audit data cannot be read or matched.",
agentExecuteTimeHint: "Agent time from schema setup through JDBC execute. Excludes row fetching; not database server execution time.",
timingNumber: "No.",
timingStage: "Stage",
timingDuration: "Time",
timingDetails: "Query timing details",
timingUnavailable: "Not measured",
timingPages: "Cumulative across {count} requests; rendering is for the latest update.",
timingPhase_request: "Request wait",
timingPhase_prepare: "Preparation (outside wait)",
timingPhase_core_lock: "Wait for execution lock",
timingPhase_pool_acquire: "Acquire connection",
timingPhase_session_prepare: "Prepare session",
timingPhase_pool_release: "Return connection",
timingPhase_schema: "Set schema",
timingPhase_statement_prepare: "Prepare statement",
timingPhase_jdbc_execute: "Execute SQL (JDBC)",
timingPhase_metadata: "Read column metadata",
timingPhase_fetch: "Read rows and close cursor",
timingPhase_agent_other: "Other Agent processing (remainder)",
timingPhase_remainder: "Other wait (remainder, not network time)",
timingPhase_result: "Process results (outside wait)",
timingBackend: "Backend-reported time (overlaps stages)",
timingPhase_render: "Update grid (outside wait)",
clientRequestWait: "Wait: {ms} ms",
clientRequestWaitHint: "Desktop time from query request dispatch until the full result payload arrives. Includes fetching, the optional server audit lookup, and transport; excludes SQL preparation and grid rendering. Summed across loaded pages.",
clientRequestWaitHint: "Desktop time from query request dispatch until the full result payload arrives. Includes backend processing, fetching, and transport; excludes request preparation and grid rendering. Summed across loaded pages.",
resultViewUpdate: "Grid update: {ms} ms",
resultViewUpdateHint: "Time from the grid observing this result until its DOM update completes or a Canvas draw is submitted. Excludes GPU compositing and screen presentation.",
querySuccess: "Query executed successfully",
+22
View File
@@ -1992,6 +1992,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"EXECUTE_TIME de OceanBase SQL Audit mide la ejecución del plan en el servidor. Los cursores JDBC muestran la muestra al finalizar la sentencia original; la paginación con consultas independientes solo muestra la suma si todas las páginas tienen muestras. Se solapa con los tiempos de ejecución y espera. No está disponible si el cursor sigue abierto o si los datos de auditoría no se pueden leer o asociar.",
agentExecuteTimeHint: "Tiempo del Agent desde la configuración del esquema hasta que retorna JDBC execute. No incluye la lectura de filas ni equivale al tiempo de ejecución del servidor de base de datos.",
timingNumber: "N.º",
timingStage: "Etapa",
timingDuration: "Tiempo",
timingDetails: "Detalles del tiempo de consulta",
timingUnavailable: "Sin medir",
timingPages: "Acumulado de {count} solicitudes; el renderizado corresponde a la última actualización.",
timingPhase_request: "Espera de la solicitud",
timingPhase_prepare: "Preparación (fuera de la espera)",
timingPhase_core_lock: "Espera del bloqueo de ejecución",
timingPhase_pool_acquire: "Obtener conexión",
timingPhase_session_prepare: "Preparar sesión",
timingPhase_pool_release: "Devolver conexión",
timingPhase_schema: "Establecer esquema",
timingPhase_statement_prepare: "Preparar sentencia",
timingPhase_jdbc_execute: "Ejecutar SQL (JDBC)",
timingPhase_metadata: "Leer metadatos de columnas",
timingPhase_fetch: "Leer filas y cerrar cursor",
timingPhase_agent_other: "Otros procesos del Agent (diferencia)",
timingPhase_remainder: "Otras esperas (diferencia, no es tiempo de red)",
timingPhase_result: "Procesar resultados (fuera de la espera)",
timingBackend: "Tiempo informado por el backend (se solapa con las etapas)",
timingPhase_render: "Actualizar tabla (fuera de la espera)",
clientRequestWait: "Espera: {ms} ms",
clientRequestWaitHint:
"Tiempo desde que la aplicación de escritorio envía la consulta hasta que recibe el resultado completo. Incluye la lectura de filas, la consulta de auditoría del servidor cuando está disponible y la transferencia; excluye la preparación del SQL y el renderizado de la tabla. Se suma entre las páginas cargadas.",
+22
View File
@@ -1926,6 +1926,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"EXECUTE_TIME di OceanBase SQL Audit misura l'esecuzione del piano sul server. I cursori JDBC mostrano il campione al termine dell'istruzione originale; la paginazione con query indipendenti mostra la somma solo se tutte le pagine hanno un campione. Si sovrappone ai tempi di esecuzione e attesa. Non è disponibile se il cursore è ancora aperto o se i dati di audit non possono essere letti o associati.",
agentExecuteTimeHint: "Tempo dell'Agent dall'impostazione dello schema al ritorno di JDBC execute. Esclude la lettura delle righe e non equivale al tempo di esecuzione del server di database.",
timingNumber: "N.",
timingStage: "Fase",
timingDuration: "Tempo",
timingDetails: "Dettagli dei tempi della query",
timingUnavailable: "Non misurato",
timingPages: "Totale di {count} richieste; il rendering si riferisce al più recente aggiornamento.",
timingPhase_request: "Attesa della richiesta",
timingPhase_prepare: "Preparazione (fuori dal tempo di attesa)",
timingPhase_core_lock: "Attesa del blocco di esecuzione",
timingPhase_pool_acquire: "Acquisizione connessione",
timingPhase_session_prepare: "Preparazione sessione",
timingPhase_pool_release: "Restituzione connessione",
timingPhase_schema: "Impostazione schema",
timingPhase_statement_prepare: "Preparazione istruzione",
timingPhase_jdbc_execute: "Esecuzione SQL (JDBC)",
timingPhase_metadata: "Lettura metadati delle colonne",
timingPhase_fetch: "Lettura righe e chiusura cursore",
timingPhase_agent_other: "Altre elaborazioni Agent (differenza)",
timingPhase_remainder: "Altre attese (differenza, non tempo di rete)",
timingPhase_result: "Elaborazione risultati (fuori dal tempo di attesa)",
timingBackend: "Tempo riportato dal backend (sovrapposto alle fasi)",
timingPhase_render: "Aggiornamento tabella (fuori dal tempo di attesa)",
clientRequestWait: "Attesa: {ms} ms",
clientRequestWaitHint:
"Tempo dall'invio della query dall'applicazione desktop alla ricezione del risultato completo. Include la lettura delle righe, la query di audit del server quando disponibile e il trasferimento; esclude la preparazione SQL e il rendering della tabella. Viene sommato tra le pagine caricate.",
+22
View File
@@ -1948,6 +1948,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"OceanBase SQL Audit の EXECUTE_TIME はサーバーでの実行計画の処理時間です。JDBC カーソルでは元の文の終了後に取得したサンプルを表示します。ページごとに独立した SQL を実行する場合は、すべてのページのサンプルがあるときだけ合計を表示します。実行時間や待機時間と範囲が重複します。カーソルが終了していない場合や、監査データの取得・関連付けができない場合は取得不可となります。",
agentExecuteTimeHint: "スキーマの設定から JDBC execute が戻るまでの Agent の処理時間です。行の取得は含まず、データベースサーバーだけの実行時間ではありません。",
timingNumber: "番号",
timingStage: "段階",
timingDuration: "時間",
timingDetails: "クエリ時間の詳細",
timingUnavailable: "未計測",
timingPages: "{count} 件のリクエストの累計。描画時間は直近の更新分です。",
timingPhase_request: "リクエスト待機",
timingPhase_prepare: "準備(待機時間に含まない)",
timingPhase_core_lock: "実行ロック待機",
timingPhase_pool_acquire: "接続の取得",
timingPhase_session_prepare: "セッションの準備",
timingPhase_pool_release: "接続の返却",
timingPhase_schema: "スキーマの設定",
timingPhase_statement_prepare: "ステートメントの準備",
timingPhase_jdbc_execute: "SQL の実行(JDBC)",
timingPhase_metadata: "列メタデータの取得",
timingPhase_fetch: "行の読み取りとカーソルの終了",
timingPhase_agent_other: "その他の Agent 処理(差分)",
timingPhase_remainder: "その他の待機(差分、通信時間ではない)",
timingPhase_result: "結果の処理(待機時間に含まない)",
timingBackend: "バックエンド報告時間(各段階と重複)",
timingPhase_render: "表の更新(待機時間に含まない)",
clientRequestWait: "待機時間:{ms} ms",
clientRequestWaitHint: "デスクトップ側がクエリ要求を送信してから結果全体を受信するまでの時間です。行の取得、利用可能な場合のサーバー監査クエリ、転送を含み、SQL の準備とテーブル描画は含みません。複数ページを読み込んだ場合は各ページの待機時間を合計します。",
resultViewUpdate: "テーブル更新:{ms} ms",
+22
View File
@@ -1877,6 +1877,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"OceanBase SQL Audit의 EXECUTE_TIME은 서버에서 실행 계획을 수행한 시간입니다. JDBC 커서는 원래 구문이 끝난 후의 샘플을 표시하며, 페이지별 독립 SQL은 모든 페이지에 샘플이 있을 때만 합계를 표시합니다. 실행 시간 및 대기 시간과 측정 범위가 겹칩니다. 커서가 끝나지 않았거나 감사 데이터를 읽거나 연결할 수 없으면 사용할 수 없습니다.",
agentExecuteTimeHint: "스키마 설정부터 JDBC execute가 반환될 때까지의 Agent 처리 시간입니다. 행 가져오기는 포함하지 않으며 데이터베이스 서버만의 실행 시간은 아닙니다.",
timingNumber: "번호",
timingStage: "단계",
timingDuration: "시간",
timingDetails: "쿼리 시간 상세",
timingUnavailable: "측정되지 않음",
timingPages: "{count}개 요청의 누적 시간입니다. 렌더링은 최근 업데이트 기준입니다.",
timingPhase_request: "요청 대기",
timingPhase_prepare: "준비 (대기 시간 제외)",
timingPhase_core_lock: "실행 잠금 대기",
timingPhase_pool_acquire: "연결 획득",
timingPhase_session_prepare: "세션 준비",
timingPhase_pool_release: "연결 반환",
timingPhase_schema: "스키마 설정",
timingPhase_statement_prepare: "문 준비",
timingPhase_jdbc_execute: "SQL 실행 (JDBC)",
timingPhase_metadata: "열 메타데이터 읽기",
timingPhase_fetch: "행 읽기 및 커서 닫기",
timingPhase_agent_other: "기타 Agent 처리 (차이값)",
timingPhase_remainder: "기타 대기 (차이값, 네트워크 시간 아님)",
timingPhase_result: "결과 처리 (대기 시간 제외)",
timingBackend: "백엔드 보고 시간 (단계별 시간과 중복)",
timingPhase_render: "표 업데이트 (대기 시간 제외)",
clientRequestWait: "대기 시간: {ms} ms",
clientRequestWaitHint: "데스크톱에서 쿼리 요청을 보낸 시점부터 전체 결과를 받을 때까지의 시간입니다. 행 가져오기, 사용 가능한 경우의 서버 감사 조회 및 전송을 포함하며 SQL 준비와 테이블 렌더링은 제외합니다. 여러 페이지를 불러오면 각 페이지의 대기 시간을 합산합니다.",
resultViewUpdate: "테이블 갱신: {ms} ms",
+22
View File
@@ -1928,6 +1928,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"EXECUTE_TIME do OceanBase SQL Audit mede a execução do plano no servidor. Os cursores JDBC mostram a amostra após o término da instrução original; a paginação com consultas independentes só mostra a soma quando todas as páginas têm amostras. Há sobreposição com os tempos de execução e espera. Indisponível enquanto o cursor não terminar ou quando os dados de auditoria não puderem ser lidos ou associados.",
agentExecuteTimeHint: "Tempo do Agent desde a configuração do esquema até o retorno de JDBC execute. Exclui a leitura de linhas e não equivale ao tempo de execução do servidor de banco de dados.",
timingNumber: "Nº",
timingStage: "Etapa",
timingDuration: "Tempo",
timingDetails: "Detalhes do tempo da consulta",
timingUnavailable: "Não medido",
timingPages: "Acumulado de {count} solicitações; a renderização corresponde à última atualização.",
timingPhase_request: "Espera da solicitação",
timingPhase_prepare: "Preparação (fora da espera)",
timingPhase_core_lock: "Espera pelo bloqueio de execução",
timingPhase_pool_acquire: "Obter conexão",
timingPhase_session_prepare: "Preparar sessão",
timingPhase_pool_release: "Devolver conexão",
timingPhase_schema: "Definir esquema",
timingPhase_statement_prepare: "Preparar instrução",
timingPhase_jdbc_execute: "Executar SQL (JDBC)",
timingPhase_metadata: "Ler metadados das colunas",
timingPhase_fetch: "Ler linhas e fechar cursor",
timingPhase_agent_other: "Outros processamentos do Agent (diferença)",
timingPhase_remainder: "Outras esperas (diferença, não é tempo de rede)",
timingPhase_result: "Processar resultados (fora da espera)",
timingBackend: "Tempo informado pelo backend (sobrepõe as etapas)",
timingPhase_render: "Atualizar tabela (fora da espera)",
clientRequestWait: "Espera: {ms} ms",
clientRequestWaitHint:
"Tempo entre o envio da consulta pelo aplicativo desktop e o recebimento do resultado completo. Inclui a leitura de linhas, a consulta de auditoria do servidor quando disponível e a transferência; exclui a preparação do SQL e a renderização da tabela. Soma os tempos de espera das páginas carregadas.",
+22
View File
@@ -1976,6 +1976,28 @@ export default withEnglishFallback({
serverExecuteTimeHint:
"OceanBase SQL Audit EXECUTE_TIME измеряет выполнение плана на сервере. Курсор JDBC показывает образец завершённого оператора; независимые запросы страниц показывают сумму только тогда, когда каждая страница попала в выборку. Эти данные пересекаются со временем выполнения и ожидания. Недоступно, пока курсор остаётся открытым или пока данные аудита нельзя прочитать или сопоставить.",
agentExecuteTimeHint: "Время агента от настройки схемы до выполнения JDBC. Не включает выборку строк; это не время выполнения на сервере БД.",
timingNumber: "№",
timingStage: "Этап",
timingDuration: "Время",
timingDetails: "Подробности времени запроса",
timingUnavailable: "Не измерено",
timingPages: "Суммарно по {count} запросам; отрисовка относится к последнему обновлению.",
timingPhase_request: "Ожидание запроса",
timingPhase_prepare: "Подготовка (вне ожидания)",
timingPhase_core_lock: "Ожидание блокировки выполнения",
timingPhase_pool_acquire: "Получение соединения",
timingPhase_session_prepare: "Подготовка сеанса",
timingPhase_pool_release: "Возврат соединения",
timingPhase_schema: "Установка схемы",
timingPhase_statement_prepare: "Подготовка оператора",
timingPhase_jdbc_execute: "Выполнение SQL (JDBC)",
timingPhase_metadata: "Чтение метаданных столбцов",
timingPhase_fetch: "Чтение строк и закрытие курсора",
timingPhase_agent_other: "Прочая обработка Agent (разница)",
timingPhase_remainder: "Прочее ожидание (разница, не время сети)",
timingPhase_result: "Обработка результатов (вне ожидания)",
timingBackend: "Время от backend (перекрывается с этапами)",
timingPhase_render: "Обновление таблицы (вне ожидания)",
clientRequestWait: "Ожидание: {ms} мс",
clientRequestWaitHint: "Время настольного приложения от отправки запроса до получения полного ответа. Включает выборку, необязательный поиск в аудите сервера и передачу данных; не включает подготовку SQL и отрисовку таблицы. Суммируется по загруженным страницам.",
resultViewUpdate: "Обновление таблицы: {ms} мс",
+23 -1
View File
@@ -2006,8 +2006,30 @@ export default withEnglishFallback({
serverExecuteTimeUnavailable: "数据库执行:不可用",
serverExecuteTimeHint: "OceanBase SQL Audit EXECUTE_TIME 表示服务端执行计划的耗时。JDBC 游标显示原语句结束后的样本;独立分页 SQL 仅在各页均有样本时显示总和。与执行耗时和等待耗时范围重叠。游标未结束或审计数据无法读取、关联时不可用。",
agentExecuteTimeHint: "从设置 schema 到 JDBC execute 返回的 Agent 耗时;不含取行,并非数据库服务端纯执行时间。",
timingNumber: "序号",
timingStage: "阶段",
timingDuration: "耗时",
timingDetails: "查询耗时详情",
timingUnavailable: "未采集",
timingPages: "累计 {count} 次请求;表格更新为最近一次。",
timingPhase_request: "请求等待",
timingPhase_prepare: "请求准备(不计入等待)",
timingPhase_core_lock: "等待执行锁",
timingPhase_pool_acquire: "获取连接",
timingPhase_session_prepare: "准备会话",
timingPhase_pool_release: "归还连接",
timingPhase_schema: "设置 schema",
timingPhase_statement_prepare: "准备语句",
timingPhase_jdbc_execute: "执行 SQL(JDBC)",
timingPhase_metadata: "读取列信息",
timingPhase_fetch: "读取结果并关闭游标",
timingPhase_agent_other: "Agent 其他处理(差额)",
timingPhase_remainder: "其他等待(差额,非网络耗时)",
timingPhase_result: "结果整理(不计入等待)",
timingBackend: "后端报告耗时(与步骤重叠)",
timingPhase_render: "表格更新(不计入等待)",
clientRequestWait: "等待耗时:{ms} 毫秒",
clientRequestWaitHint: "桌面端发出查询请求至收到完整结果的耗时,包含取行、可用时的服务端审计查询和传输,不含 SQL 准备与表格渲染;加载多页时累加各页等待时间。",
clientRequestWaitHint: "桌面端发出查询请求至收到完整结果的耗时,包含后端处理、取行和传输,不含请求准备与表格渲染;加载多页时累加各页等待时间。",
resultViewUpdate: "表格更新:{ms} 毫秒",
resultViewUpdateHint: "从表格接收到本次结果,到 DOM 更新完成或 Canvas 绘制提交;不含 GPU 合成及屏幕实际呈现。",
querySuccess: "查询执行成功",
+23 -1
View File
@@ -1951,8 +1951,30 @@ export default withEnglishFallback({
serverExecuteTimeUnavailable: "資料庫執行:無法取得",
serverExecuteTimeHint: "OceanBase SQL Audit EXECUTE_TIME 表示伺服端執行計畫的耗時。JDBC 游標顯示原語句結束後的樣本;獨立分頁 SQL 僅在各頁均有樣本時顯示總和。與執行耗時和等待耗時範圍重疊。游標尚未結束或稽核資料無法讀取、關聯時無法取得。",
agentExecuteTimeHint: "從設定 schema 到 JDBC execute 返回的 Agent 耗時;不含讀取資料列,並非資料庫伺服端純執行時間。",
timingNumber: "序號",
timingStage: "階段",
timingDuration: "耗時",
timingDetails: "查詢耗時詳情",
timingUnavailable: "未採集",
timingPages: "累計 {count} 次請求;表格更新為最近一次。",
timingPhase_request: "請求等待",
timingPhase_prepare: "請求準備(不計入等待)",
timingPhase_core_lock: "等待執行鎖",
timingPhase_pool_acquire: "取得連線",
timingPhase_session_prepare: "準備工作階段",
timingPhase_pool_release: "歸還連線",
timingPhase_schema: "設定 schema",
timingPhase_statement_prepare: "準備語句",
timingPhase_jdbc_execute: "執行 SQL(JDBC)",
timingPhase_metadata: "讀取欄位資訊",
timingPhase_fetch: "讀取結果並關閉游標",
timingPhase_agent_other: "Agent 其他處理(差額)",
timingPhase_remainder: "其他等待(差額,非網路耗時)",
timingPhase_result: "結果整理(不計入等待)",
timingBackend: "後端回報耗時(與步驟重疊)",
timingPhase_render: "表格更新(不計入等待)",
clientRequestWait: "等待耗時:{ms} 毫秒",
clientRequestWaitHint: "桌面端送出查詢請求至收到完整結果的耗時,包含讀取資料列、可用時的伺服端稽核查詢與傳輸,不含 SQL 準備與表格呈現;載入多頁時累加各頁等待時間。",
clientRequestWaitHint: "桌面端送出查詢請求至收到完整結果的耗時,包含後端處理、讀取資料列與傳輸,不含請求準備與表格呈現;載入多頁時累加各頁等待時間。",
resultViewUpdate: "表格更新:{ms} 毫秒",
resultViewUpdateHint: "從表格接收到本次結果,到 DOM 更新完成或 Canvas 繪製提交;不含 GPU 合成及螢幕實際呈現。",
querySuccess: "查詢執行成功",
@@ -0,0 +1,30 @@
import { afterEach, expect, it, vi } from "vitest";
import { createQueryRequestTiming } from "../queryRequestTiming";
afterEach(() => vi.restoreAllMocks());
it("separates preparation, backend waits and local conversion across multiple calls", async () => {
let now = 100;
vi.spyOn(performance, "now").mockImplementation(() => now);
const timing = createQueryRequestTiming(80);
const value = await timing.run(async () => {
now = 130;
return "result";
});
expect(value).toBe("result");
now = 140;
await timing.run(async () => {
now = 160;
});
now = 165;
const result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 99 };
expect(timing.finish(result)).toMatchObject({ client_prepare_ms: 20, client_request_wait_ms: 50, client_result_ms: 15, timing_page_count: 2, execution_time_ms: 99 });
});
it("does not invent request timings for a local command and propagates failures without replay", async () => {
const result = { columns: [], rows: [], affected_rows: 0, execution_time_ms: 0 };
expect(createQueryRequestTiming(0).finish(result)).not.toHaveProperty("client_request_wait_ms");
const call = vi.fn().mockRejectedValue(new Error("query failed"));
await expect(createQueryRequestTiming(0).run(call)).rejects.toThrow("query failed");
expect(call).toHaveBeenCalledTimes(1);
});
@@ -17,6 +17,18 @@ function backend(name: ResultCacheBackend["name"], overrides: Partial<ResultCach
}
describe("tab result cache statement execution metadata", () => {
it("retains timing details for active, array and historical results through cache restore", async () => {
const { buildTabResultSnapshot } = await import("@/lib/tabs/tabResultCache");
const timing = { query_timings_ms: { agent_total: 40, pool_acquire: 20, pool_release: 2 }, client_prepare_ms: 3, client_result_ms: 4, timing_page_count: 2 };
const result = { columns: ["N"], rows: [[1]], affected_rows: 0, execution_time_ms: 50, ...timing };
const tab = { result, results: [result], resultRuns: [{ id: "timed-run", title: "Run", sequence: 1, sql: "SELECT 1", createdAt: 1, result, results: [result] }] };
const snapshot = buildTabResultSnapshot(tab as Parameters<typeof buildTabResultSnapshot>[0])!;
expect(snapshot.result?.query_timings_ms).not.toBe(result.query_timings_ms);
const restored = decodeTabResultSnapshot(encodeTabResultSnapshot(snapshot));
for (const value of [restored.result, restored.results?.[0], restored.resultRuns?.[0].result, restored.resultRuns?.[0].results?.[0]]) {
expect(value).toMatchObject({ rows: [[1]], ...timing });
}
});
it("selects one authoritative backend before fallback", () => {
expect(resultCacheBackendOrder(true)).toEqual(["runtime", "indexed-db"]);
expect(resultCacheBackendOrder(false)).toEqual(["indexed-db", "runtime"]);
@@ -0,0 +1,28 @@
import type { QueryResult } from "@/types/database";
/** Local command timing; run wraps only backend calls, never result conversion. */
export function createQueryRequestTiming(prepareStartedAt: number) {
let firstRequestAt: number | undefined;
let waitMs = 0;
let requests = 0;
return {
async run<T, A extends unknown[]>(request: (...args: A) => Promise<T>, ...args: A): Promise<T> {
const startedAt = performance.now();
firstRequestAt ??= startedAt;
requests += 1;
try {
return await request(...args);
} finally {
waitMs += Math.max(0, performance.now() - startedAt);
}
},
finish<T extends QueryResult>(result: T): T {
if (firstRequestAt === undefined || result.execution_error) return result;
result.client_prepare_ms = Math.max(0, firstRequestAt - prepareStartedAt);
result.client_request_wait_ms = waitMs;
result.client_result_ms = Math.max(0, performance.now() - firstRequestAt - waitMs);
result.timing_page_count = requests;
return result;
},
};
}
@@ -71,6 +71,10 @@ interface ColumnarQueryResult {
execution_time_ms: number;
server_execute_time_us?: number;
client_request_wait_ms?: number;
query_timings_ms?: QueryResult["query_timings_ms"];
client_prepare_ms?: number;
client_result_ms?: number;
timing_page_count?: number;
truncated?: boolean;
has_more?: boolean;
sourceLabel?: string;
@@ -361,6 +365,10 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
execution_time_ms: result.execution_time_ms,
server_execute_time_us: result.server_execute_time_us,
client_request_wait_ms: result.client_request_wait_ms,
query_timings_ms: result.query_timings_ms ? { ...result.query_timings_ms } : undefined,
client_prepare_ms: result.client_prepare_ms,
client_result_ms: result.client_result_ms,
timing_page_count: result.timing_page_count,
truncated: result.truncated,
session_id: undefined,
has_more: result.has_more,
@@ -420,6 +428,10 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
execution_time_ms: result.execution_time_ms,
server_execute_time_us: result.server_execute_time_us,
client_request_wait_ms: result.client_request_wait_ms,
query_timings_ms: result.query_timings_ms ? { ...result.query_timings_ms } : undefined,
client_prepare_ms: result.client_prepare_ms,
client_result_ms: result.client_result_ms,
timing_page_count: result.timing_page_count,
truncated: result.truncated,
has_more: result.has_more,
sourceLabel: result.sourceLabel,
@@ -451,6 +463,10 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
execution_time_ms: result.execution_time_ms,
server_execute_time_us: result.server_execute_time_us,
client_request_wait_ms: result.client_request_wait_ms,
query_timings_ms: result.query_timings_ms ? { ...result.query_timings_ms } : undefined,
client_prepare_ms: result.client_prepare_ms,
client_result_ms: result.client_result_ms,
timing_page_count: result.timing_page_count,
truncated: result.truncated,
session_id: undefined,
has_more: result.has_more,
@@ -80,6 +80,9 @@ describe("queryStore MongoDB execution summary", () => {
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.result?.rows).toHaveLength(3);
expect(tab.result?.client_request_wait_ms).toEqual(expect.any(Number));
expect(tab.result?.client_result_ms).toEqual(expect.any(Number));
expect(tab.result?.timing_page_count).toBe(1);
expect(tab.batchSqlExecution).toMatchObject({
total: 1,
completed: 1,
@@ -113,6 +113,24 @@ function structuredSqlError(detail = "duplicate key") {
}
describe("queryStore multi-statement errors", () => {
it.each(["oracle", "oceanbase-oracle"])("preserves the intended offset timing scope for %s", async (dbType) => {
mocks.getConnectionConfig.mockReturnValue({ id: "timing-offset", name: "Timing", db_type: dbType, database: "APP", query_timeout_secs: 30 });
mocks.analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "complex-query" });
mocks.prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({ sqlToExecute: options.sql, pageSql: options.sql, pageLimit: options.pagination.limit, pageOffset: options.pagination.offset, countSql: undefined, useAgentResultSession: true }));
const timed = true;
mocks.executeMulti
.mockResolvedValueOnce([{ columns: ["VALUE"], rows: [[1], [2]], affected_rows: 0, execution_time_ms: 12, session_id: "offset-page", has_more: true, ...(timed ? { query_timings_ms: { agent_total: 10 } } : {}) }])
.mockResolvedValueOnce([{ columns: ["VALUE"], rows: [[3], [4]], affected_rows: 0, execution_time_ms: 34, has_more: false, ...(timed ? { query_timings_ms: { agent_total: 30 } } : {}) }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("timing-offset", "APP", "Query", "query", "APP");
await store.executeTabSql(tabId, "SELECT VALUE FROM T", { pagination: { limit: 2, offset: 2 } });
const result = store.tabs.find((item) => item.id === tabId)!.result!;
expect(result.rows).toEqual([[3], [4]]);
expect(result.execution_time_ms).toBe(timed ? 46 : 34);
expect(result.query_timings_ms).toEqual(timed ? { agent_total: 40 } : undefined);
expect(result.timing_page_count).toBe(timed ? 2 : undefined);
});
beforeEach(() => {
vi.clearAllMocks();
mocks.defaultAutoKeepResults = false;
@@ -160,7 +178,7 @@ describe("queryStore multi-statement errors", () => {
});
});
it.each(["oceanbase-oracle", "mysql"] as const)("measures complete result wait only for a single %s query result", async (databaseType) => {
it.each(["oceanbase-oracle", "oracle", "mysql", "postgres", "sqlite", "sqlserver", "db2"] as const)("measures complete result wait only for a single %s query result", async (databaseType) => {
mocks.getConnectionConfig.mockReturnValue({
id: "timing-1",
name: "Timing",
@@ -183,7 +201,7 @@ describe("queryStore multi-statement errors", () => {
await execution;
const result = store.tabs.find((item) => item.id === tabId)?.result;
expect(result?.execution_time_ms).toBe(12);
expect(result?.client_request_wait_ms).toBe(databaseType === "oceanbase-oracle" ? 45 : undefined);
expect(result?.client_request_wait_ms).toBe(45);
} finally {
vi.restoreAllMocks();
}
+75 -36
View File
@@ -1,3 +1,4 @@
import { createQueryRequestTiming } from "@/lib/queryRequestTiming";
import { UPDATE_RESTORE_KEY, assertUpdateAllowsInteraction } from "@/lib/app/updatePreparation";
import { defineStore } from "pinia";
import { isRedisMonitorCommand, startRedisMonitor } from "@/lib/redis/redisMonitor";
@@ -346,6 +347,11 @@ export function appendQueryResultSegment(previous: QueryResult, segment: QueryRe
// Independent page SQLs need every page sampled before a total is shown.
server_execute_time_us: previous.session_id ? segment.server_execute_time_us : previous.server_execute_time_us !== undefined && segment.server_execute_time_us !== undefined ? previous.server_execute_time_us + segment.server_execute_time_us : undefined,
client_request_wait_ms: previous.client_request_wait_ms !== undefined && segment.client_request_wait_ms !== undefined ? previous.client_request_wait_ms + segment.client_request_wait_ms : undefined,
query_timings_ms:
previous.query_timings_ms && segment.query_timings_ms ? Object.fromEntries([...new Set([...Object.keys(previous.query_timings_ms), ...Object.keys(segment.query_timings_ms)])].map((key) => [key, (previous.query_timings_ms![key] ?? 0) + (segment.query_timings_ms![key] ?? 0)])) : undefined,
client_prepare_ms: previous.client_prepare_ms !== undefined && segment.client_prepare_ms !== undefined ? previous.client_prepare_ms + segment.client_prepare_ms : undefined,
client_result_ms: previous.client_result_ms !== undefined && segment.client_result_ms !== undefined ? previous.client_result_ms + segment.client_result_ms : undefined,
timing_page_count: (previous.timing_page_count ?? 1) + (segment.timing_page_count ?? 1),
has_more: previous.rows.length + appendedRowCount >= maxRows ? false : segment.has_more,
});
}
@@ -6906,11 +6912,12 @@ export const useQueryStore = defineStore("query", () => {
const skipSafety = options?.skipRedisSafetyCheck;
let hadMutatingCommand = false;
for (const [commandIndex, command] of commands.entries()) {
const commandTiming = createQueryRequestTiming(commandIndex === 0 ? startedAt : performance.now());
const commandRange = commandRanges[commandIndex];
const sourceRange = commandRange && options?.sourceOffset !== undefined ? { from: options.sourceOffset + commandRange.from, to: options.sourceOffset + commandRange.to } : undefined;
try {
const result = await api.redisExecuteCommand(executionConnectionId, currentDb, command, skipSafety);
allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, command), command, undefined, undefined, sourceRange)));
const result = await commandTiming.run(api.redisExecuteCommand, executionConnectionId, currentDb, command, skipSafety);
allResults.push(commandTiming.finish(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, command), command, undefined, undefined, sourceRange))));
// Track db switches from SELECT N so later commands in the same batch run on the right db.
currentDb = nextRedisCommandDb(currentDb, command, result.value);
// Write commands (SET/DEL/...) mutate the key set — drop the cached key-name completion
@@ -6986,7 +6993,7 @@ export const useQueryStore = defineStore("query", () => {
let mongoEditTarget: QueryTab["mongoEditTarget"] | undefined;
let mongoFindPageState: { pageLimit: number; pageOffset: number; total: number; totalIsExact: boolean } | undefined;
for (const parsedCommand of mongoCommands) {
for (const [commandIndex, parsedCommand] of mongoCommands.entries()) {
let mongoCommand = parsedCommand.command;
// db.getSiblingDB("x").<command>: target that database for this command only.
const sessionDatabase = currentDatabase;
@@ -6998,6 +7005,7 @@ export const useQueryStore = defineStore("query", () => {
const sourceStatement = parsedCommand.text;
const sourceRange = options?.sourceOffset === undefined ? undefined : { from: options.sourceOffset + parsedCommand.from, to: options.sourceOffset + parsedCommand.to };
const commandStartedAt = performance.now();
const commandTiming = createQueryRequestTiming(commandIndex === 0 ? startedAt : commandStartedAt);
const annotateMongoResult = (result: QueryResult): QueryResult => {
const annotated = annotateQueryResultSource(result, sourceStatement, undefined, undefined, sourceRange);
if ("collection" in mongoCommand) {
@@ -7005,7 +7013,7 @@ export const useQueryStore = defineStore("query", () => {
annotated.sourceName = mongoCommand.collection;
annotated.sourceLabel = currentDatabase ? `${currentDatabase}.${mongoCommand.collection}` : mongoCommand.collection;
}
return annotated;
return commandTiming.finish(annotated);
};
try {
// The frontend parser remains responsible for editor ranges, while
@@ -7029,7 +7037,19 @@ export const useQueryStore = defineStore("query", () => {
if (!pagePlan) throw new Error(describeMongoCommandParseFailure(sourceStatement));
// A stale request can point past an explicit .limit() bound. Keep
// the backend call bounded so limit(0) cannot become unbounded.
const result = await api.mongoFindDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, pagePlan.requestSkip, Math.max(1, pagePlan.requestLimit), mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, mongoCommand.collation, executionId);
const result = await commandTiming.run(
api.mongoFindDocuments,
executionConnectionId,
currentDatabase,
mongoCommand.collection,
pagePlan.requestSkip,
Math.max(1, pagePlan.requestLimit),
mongoCommand.filter,
mongoCommand.projection,
mongoCommand.sort,
mongoCommand.collation,
executionId,
);
const documents = pagePlan.requestLimit === 0 ? [] : result.documents;
const extendedDocuments = pagePlan.requestLimit === 0 ? [] : result.extended_documents;
const totalIsExact = result.total_is_exact !== false;
@@ -7057,7 +7077,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "findOne": {
queryExecutionLog("info", "mongo-find-one:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const result = await api.mongoFindOne(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId);
const result = await commandTiming.run(api.mongoFindOne, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)));
allResults.push(queryResult);
mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
@@ -7072,7 +7092,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "version": {
queryExecutionLog("info", "mongo-version:start", { traceId, database: currentDatabase });
const version = await api.mongoServerVersion(executionConnectionId, currentDatabase, executionId);
const version = await commandTiming.run(api.mongoServerVersion, executionConnectionId, currentDatabase, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoVersionToQueryResult(version, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-version:done", {
@@ -7085,7 +7105,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "showDatabases": {
queryExecutionLog("info", "mongo-show-databases:start", { traceId });
const result = await api.mongoRunCommand(executionConnectionId, "admin", '{"listDatabases":1}', executionId);
const result = await commandTiming.run(api.mongoRunCommand, executionConnectionId, "admin", '{"listDatabases":1}', executionId);
const queryResult = mongoDatabasesToQueryResult(result.documents, performance.now() - commandStartedAt, agentProtocolQueryResultMaxRows(mongoResultMaxRows));
allResults.push(markQueryResultRowsRaw(annotateMongoResult(queryResult)));
mongoEditTarget = undefined;
@@ -7098,7 +7118,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "countDocuments": {
queryExecutionLog("info", "mongo-count:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const total = await api.mongoCountDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.mode, executionId);
const total = await commandTiming.run(api.mongoCountDocuments, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.mode, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCountToQueryResult(total, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-count:done", {
@@ -7112,7 +7132,8 @@ export const useQueryStore = defineStore("query", () => {
}
case "findExplain": {
queryExecutionLog("info", "mongo-explain:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const plan = await api.mongoExplainFind(
const plan = await commandTiming.run(
api.mongoExplainFind,
executionConnectionId,
currentDatabase,
mongoCommand.collection,
@@ -7139,7 +7160,7 @@ export const useQueryStore = defineStore("query", () => {
}
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(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, mongoCommand.options, executionId);
const result = await commandTiming.run(api.mongoAggregateDocuments, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, mongoCommand.options, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-aggregate:done", {
@@ -7154,7 +7175,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "distinct": {
queryExecutionLog("info", "mongo-distinct:start", { traceId, collection: mongoCommand.collection, database: currentDatabase, field: mongoCommand.field });
const result = await api.mongoDistinct(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.field, mongoCommand.filter, executionId);
const result = await commandTiming.run(api.mongoDistinct, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.field, mongoCommand.filter, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDistinctToQueryResult(mongoCommand.field, result.documents, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-distinct:done", {
@@ -7169,7 +7190,7 @@ export const useQueryStore = defineStore("query", () => {
}
case "getIndexes": {
queryExecutionLog("info", "mongo-indexes:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const indexes = await api.listIndexes(executionConnectionId, currentDatabase, "", mongoCommand.collection);
const indexes = await commandTiming.run(api.listIndexes, executionConnectionId, currentDatabase, "", mongoCommand.collection);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoIndexesToQueryResult(indexes, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-indexes:done", {
@@ -7188,7 +7209,7 @@ export const useQueryStore = defineStore("query", () => {
metric: mongoCommand.metric,
database: currentDatabase,
});
const stats = await api.mongoCollectionStats(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.scale, executionId);
const stats = await commandTiming.run(api.mongoCollectionStats, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.scale, executionId);
// SAFETY: The backend returns collection statistics as a JSON object; the API type is broader than the converter's record-shaped input.
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCollectionStatsToQueryResult(mongoCommand.metric, stats as unknown as Record<string, unknown>, performance.now() - commandStartedAt))));
mongoEditTarget = undefined;
@@ -7216,10 +7237,10 @@ export const useQueryStore = defineStore("query", () => {
});
const result =
mongoCommand.kind === "findOneAndUpdate"
? await api.mongoFindOneAndUpdate(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.options)
? await commandTiming.run(api.mongoFindOneAndUpdate, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.options)
: mongoCommand.kind === "findOneAndReplace"
? await api.mongoFindOneAndReplace(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options)
: await api.mongoFindOneAndDelete(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options);
? await commandTiming.run(api.mongoFindOneAndReplace, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options)
: await commandTiming.run(api.mongoFindOneAndDelete, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-find-and-modify:done", {
@@ -7241,7 +7262,7 @@ export const useQueryStore = defineStore("query", () => {
traceId,
database: currentDatabase,
});
const result = await api.mongoRunCommand(executionConnectionId, currentDatabase, mongoCommand.commandJson, executionId);
const result = await commandTiming.run(api.mongoRunCommand, executionConnectionId, currentDatabase, mongoCommand.commandJson, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-run-command:done", {
@@ -7275,38 +7296,38 @@ export const useQueryStore = defineStore("query", () => {
});
mongoEditTarget = undefined;
if (mongoCommand.kind === "insert") {
const result = await api.mongoInsertDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.docsJson);
const result = await commandTiming.run(api.mongoInsertDocuments, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.docsJson);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "update") {
const result = await api.mongoUpdateDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.many, mongoCommand.options);
const result = await commandTiming.run(api.mongoUpdateDocuments, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.update, mongoCommand.many, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "bulkWrite") {
const result = await api.mongoBulkWrite(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.operations, mongoCommand.options);
const result = await commandTiming.run(api.mongoBulkWrite, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.operations, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoBulkWriteToQueryResult(result, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "replace") {
const result = await api.mongoReplaceDocument(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options);
const result = await commandTiming.run(api.mongoReplaceDocument, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "createIndex") {
const result = await api.mongoCreateIndex(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options);
const result = await commandTiming.run(api.mongoCreateIndex, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "createUser") {
const result = await api.mongoCreateUser(executionConnectionId, currentDatabase, mongoCommand.userJson, mongoCommand.writeConcernJson);
const result = await commandTiming.run(api.mongoCreateUser, executionConnectionId, currentDatabase, mongoCommand.userJson, mongoCommand.writeConcernJson);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") {
try {
const result = await api.mongoDropIndexes(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex");
const result = await commandTiming.run(api.mongoDropIndexes, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex");
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDroppedIndexesToQueryResult(result.dropped_names, performance.now() - commandStartedAt, result.failures))));
} finally {
await refreshLoadedMongoIndexesAfterMutation(executionConnectionId, currentDatabase, mongoCommand.collection, traceId);
}
} else if (mongoCommand.kind === "renameCollection") {
await api.mongoRenameCollection(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.newName);
await commandTiming.run(api.mongoRenameCollection, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.newName);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoScalarToQueryResult("renamed", `${mongoCommand.collection} -> ${mongoCommand.newName}`, performance.now() - commandStartedAt))));
} else if (mongoCommand.kind === "dropCollection") {
await api.mongoDropCollection(executionConnectionId, currentDatabase, mongoCommand.collection);
await commandTiming.run(api.mongoDropCollection, executionConnectionId, currentDatabase, mongoCommand.collection);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(1, performance.now() - commandStartedAt))));
} else {
const result = await api.mongoDeleteDocuments(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many);
const result = await commandTiming.run(api.mongoDeleteDocuments, executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.many);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt))));
}
queryExecutionLog("info", "mongo-write:done", {
@@ -7420,15 +7441,16 @@ export const useQueryStore = defineStore("query", () => {
});
const allResults: QueryResult[] = [];
const continueOnError = continueOnBatchError;
for (const request of elasticsearchRequests) {
for (const [requestIndex, request] of elasticsearchRequests.entries()) {
const commandTiming = createQueryRequestTiming(requestIndex === 0 ? startedAt : performance.now());
const current = findExecutionTab(id);
if (current?.executionId !== executionId) break;
const sourceRange = options?.sourceOffset === undefined ? undefined : { from: options.sourceOffset + request.from, to: options.sourceOffset + request.to };
try {
const result = await api.executeQuery(executionConnectionId, executionDatabase, request.sql, undefined, executionId, {
const result = await commandTiming.run(api.executeQuery, executionConnectionId, executionDatabase, request.sql, undefined, executionId, {
timeoutSecs: queryTimeoutSecs,
});
allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(result, request.sql, targetDatabase || conn?.database, effectiveDbType, sourceRange)));
allResults.push(commandTiming.finish(markQueryResultRowsRaw(annotateQueryResultSource(result, request.sql, targetDatabase || conn?.database, effectiveDbType, sourceRange))));
if (elasticsearchHttpErrorStatus(result) !== undefined && !continueOnError) break;
} catch (error) {
const latest = findExecutionTab(id);
@@ -7613,11 +7635,15 @@ export const useQueryStore = defineStore("query", () => {
clientSession: Boolean(executionClientSessionId),
});
executionDispatched = true;
if (effectiveDbType === "oceanbase-oracle" && tab.mode === "query") clientRequestStartedAt = performance.now();
if (tab.mode === "query") clientRequestStartedAt = performance.now();
if (useAgentResultSession && tab.mode === "query" && typeof pageOffset === "number" && pageOffset > 0 && !options?.pagination?.sessionId && !(tab.batchSqlExecution && tab.batchSqlExecution.total > 1)) {
return (async () => {
let sessionId: string | undefined;
let skipped = 0;
let pageCount = 0;
let executionMs = 0;
let completeTimings = true;
const timings: Record<string, number> = {};
while (true) {
const pageResults = await api.executeMulti(executionConnectionId, executionDatabase, sqlToExecute, executionSchema, executionId, {
...executionOptions,
@@ -7625,10 +7651,17 @@ export const useQueryStore = defineStore("query", () => {
});
const page = pageResults[0];
if (!page) return pageResults;
pageCount += 1;
executionMs += page.execution_time_ms;
if (!page.query_timings_ms) completeTimings = false;
else for (const [key, value] of Object.entries(page.query_timings_ms)) timings[key] = (timings[key] ?? 0) + value;
// Offset jumps consume several cursor pages before publication.
// Keep their timings as well, without retaining skipped rows.
const timingSummary = { query_timings_ms: completeTimings ? { ...timings } : undefined, execution_time_ms: executionMs, timing_page_count: pageCount };
if (skipped + page.rows.length > pageOffset) {
const start = pageOffset - skipped;
const limit = typeof pageLimit === "number" ? pageLimit : page.rows.length - start;
return [{ ...page, rows: page.rows.slice(start, start + limit) }];
return [{ ...page, ...timingSummary, rows: page.rows.slice(start, start + limit) }];
}
skipped += page.rows.length;
if (!page.has_more || !page.session_id) {
@@ -7637,7 +7670,7 @@ export const useQueryStore = defineStore("query", () => {
// returning the short page would show the wrong rows.
throw new Error("Result session ended before the requested page offset");
}
return [{ ...page, rows: [] }];
return [{ ...page, ...timingSummary, rows: [] }];
}
sessionId = page.session_id;
}
@@ -7686,7 +7719,7 @@ export const useQueryStore = defineStore("query", () => {
} else {
queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
executionDispatched = true;
if (effectiveDbType === "oceanbase-oracle" && tab.mode === "query") clientRequestStartedAt = performance.now();
if (tab.mode === "query") clientRequestStartedAt = performance.now();
// Only an initial manual execution classifies the user SQL (sticky
// proven-read-only dialects). A later cursor-page fetch must neither
// set nor clear the sticky bit.
@@ -7741,8 +7774,11 @@ export const useQueryStore = defineStore("query", () => {
// A single result has an unambiguous request boundary. This includes fetch and
// transport, but excludes SQL preparation and the grid's later render work.
if (clientRequestStartedAt !== undefined && responseResults.length === 1 && !responseResults[0]?.execution_error) {
responseResults[0]!.client_request_wait_ms = Math.max(0, Math.round(performance.now() - clientRequestStartedAt));
responseResults[0]!.client_request_wait_ms = Math.max(0, performance.now() - clientRequestStartedAt);
responseResults[0]!.client_prepare_ms = Math.max(0, clientRequestStartedAt - startedAt);
responseResults[0]!.timing_page_count ??= 1;
}
const resultProcessingStartedAt = performance.now();
const annotatedResults = annotateQueryResultSources(markQueryResultsRowsRaw(responseResults), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset, sqlStatementParameterOptions, sqlToExecute, options?.sourceOffset === undefined ? undefined : tab.sql);
const results = offsetBatchQueryResultIndexes(annotatedResults.results, batchResume?.startStatementIndex ?? 0);
if (paginationRowNumberColumn && results.length === 1) {
@@ -7822,6 +7858,9 @@ export const useQueryStore = defineStore("query", () => {
current.database = mysqlUseDatabase;
current.schema = undefined;
}
if (clientRequestStartedAt !== undefined && results.length === 1 && !results[0]?.execution_error) {
results[0]!.client_result_ms = Math.max(0, performance.now() - resultProcessingStartedAt);
}
const activeGroupIndex = current.activeResultIndex;
const activeGroupResults = current.results;
const shouldAppendResult = !!options?.appendResult && !!current.result;
+6 -1
View File
@@ -1272,8 +1272,13 @@ export interface QueryResult {
execution_time_ms: number;
/** OceanBase SQL Audit EXECUTE_TIME for a completed statement, in microseconds. */
server_execute_time_us?: number;
/** Desktop wait from query request dispatch to the complete result payload; summed across appended pages. OceanBase Oracle query tabs only. */
/** Desktop wait from query request dispatch to the complete result payload; summed across appended pages. Completed query/command requests only. */
client_request_wait_ms?: number;
/** Measured phases; totals overlap. Missing phases were not measured. */
query_timings_ms?: Record<string, number>;
client_prepare_ms?: number;
client_result_ms?: number;
timing_page_count?: number;
/** Whether a backend-reported result total is exact. */
total_is_exact?: boolean;
truncated?: boolean;