mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:28:51 +00:00
feat(query): show result source ranges
This commit is contained in:
@@ -249,8 +249,10 @@ let codeMirrorInsertNewlineKeepIndent: typeof import("@codemirror/commands").ins
|
||||
let codeMirrorToggleLineComment: typeof import("@codemirror/commands").toggleLineComment | null = null;
|
||||
let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSemanticDiagnostic[]> | null = null;
|
||||
let setPreviewRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let setResultSourceRangeEffect: import("@codemirror/state").StateEffectType<{ from: number; to: number } | null> | null = null;
|
||||
let previewRangeComp: import("@codemirror/state").Compartment | null = null;
|
||||
let buildPreviewRangeExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildResultSourceRangeExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildRunStatementGutterExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let indentComp: import("@codemirror/state").Compartment | null = null;
|
||||
let codeMirrorIndentUnit: typeof import("@codemirror/language").indentUnit | null = null;
|
||||
@@ -491,6 +493,33 @@ function setPreviewRange(range: { from: number; to: number } | null) {
|
||||
});
|
||||
}
|
||||
|
||||
function setResultSourceRange(range: { from: number; to: number } | null) {
|
||||
if (!view.value || !setResultSourceRangeEffect) return;
|
||||
view.value.dispatch({
|
||||
effects: setResultSourceRangeEffect.of(range),
|
||||
});
|
||||
}
|
||||
|
||||
function previewStatementRange(range: { from: number; to: number } | null) {
|
||||
const currentView = view.value;
|
||||
if (!range || !currentView || !editorViewModule || !setResultSourceRangeEffect) {
|
||||
setResultSourceRange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const from = Math.max(0, Math.min(range.from, currentView.state.doc.length));
|
||||
const to = Math.max(from, Math.min(range.to, currentView.state.doc.length));
|
||||
if (from === to) {
|
||||
setResultSourceRange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
currentView.dispatch({
|
||||
selection: { anchor: from },
|
||||
effects: [setResultSourceRangeEffect.of({ from, to }), editorViewModule.EditorView.scrollIntoView(from, { y: "center" })],
|
||||
});
|
||||
}
|
||||
|
||||
function onPickerActiveIndexChange(index: number) {
|
||||
pickerActiveIndex.value = index;
|
||||
const candidate = pickerCandidates.value[index];
|
||||
@@ -2574,8 +2603,8 @@ onMounted(async () => {
|
||||
if (!editorRef.value) return;
|
||||
|
||||
const [
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, Decoration, tooltips, gutter, GutterMarker, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, ViewPlugin },
|
||||
{ EditorState, EditorSelection, Compartment, Prec, StateEffect, StateField },
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, Decoration, tooltips, gutter, GutterMarker, lineNumberMarkers, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, crosshairCursor, ViewPlugin },
|
||||
{ EditorState, EditorSelection, Compartment, Prec, RangeSet, StateEffect, StateField },
|
||||
langSql,
|
||||
{ autocompletion, startCompletion, acceptCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus, completionKeymap, insertCompletionText, nextSnippetField },
|
||||
{ copyLineDown, copyLineUp, deleteLine, indentLess, indentMore, insertNewlineKeepIndent, moveLineDown, moveLineUp, redo, selectAll, undo, toggleLineComment, history, defaultKeymap, historyKeymap },
|
||||
@@ -2681,6 +2710,7 @@ onMounted(async () => {
|
||||
return Decoration.set([Decoration.mark({ class: "cm-db-execution-preview" }).range(range.from, range.to)]);
|
||||
}
|
||||
}
|
||||
if (transaction.docChanged || transaction.selection) return Decoration.none;
|
||||
return decorations;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
@@ -2688,6 +2718,41 @@ onMounted(async () => {
|
||||
return field;
|
||||
};
|
||||
|
||||
class ResultSourceLineNumberMarker extends GutterMarker {
|
||||
elementClass = "cm-db-result-source-line-number";
|
||||
}
|
||||
|
||||
const resultSourceLineNumberMarker = new ResultSourceLineNumberMarker();
|
||||
setResultSourceRangeEffect = StateEffect.define<{ from: number; to: number } | null>();
|
||||
buildResultSourceRangeExtension = () => {
|
||||
const effectType = setResultSourceRangeEffect!;
|
||||
const markersForRange = (state: import("@codemirror/state").EditorState, range: { from: number; to: number }) => {
|
||||
const from = Math.max(0, Math.min(range.from, state.doc.length));
|
||||
const to = Math.max(from, Math.min(range.to, state.doc.length));
|
||||
const startLine = state.doc.lineAt(from);
|
||||
const endLine = state.doc.lineAt(Math.max(from, to - 1));
|
||||
const markers = Array.from({ length: endLine.number - startLine.number + 1 }, (_, index) => resultSourceLineNumberMarker.range(state.doc.line(startLine.number + index).from));
|
||||
return RangeSet.of(markers);
|
||||
};
|
||||
|
||||
const field = StateField.define({
|
||||
create() {
|
||||
return RangeSet.empty;
|
||||
},
|
||||
update(markers, transaction) {
|
||||
for (const effect of transaction.effects) {
|
||||
if (effect.is(effectType)) {
|
||||
return effect.value ? markersForRange(transaction.state, effect.value) : RangeSet.empty;
|
||||
}
|
||||
}
|
||||
if (transaction.docChanged || transaction.selection) return RangeSet.empty;
|
||||
return markers;
|
||||
},
|
||||
provide: (field) => lineNumberMarkers.from(field),
|
||||
});
|
||||
return field;
|
||||
};
|
||||
|
||||
buildSqlSignatureExtension = () =>
|
||||
showTooltip.compute(["doc", "selection"], (currentState) => {
|
||||
const signature = getSqlFunctionSignatureHelp(currentState.doc.toString(), currentState.selection.main.head);
|
||||
@@ -2878,6 +2943,7 @@ onMounted(async () => {
|
||||
requestTableColumns: requestInsertValueHintTableColumns,
|
||||
}),
|
||||
previewRangeComp.of(buildPreviewRangeExtension()),
|
||||
buildResultSourceRangeExtension(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: "'", run: handleSqlSingleQuote },
|
||||
@@ -3462,7 +3528,7 @@ function scrollCursorIntoView() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition });
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition, previewStatementRange });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -3495,6 +3561,15 @@ defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pa
|
||||
background: var(--dbx-editor-selection-background, rgba(59, 130, 246, 0.35));
|
||||
}
|
||||
|
||||
:deep(.cm-lineNumbers .cm-db-result-source-line-number) {
|
||||
color: rgb(126 34 206) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:global(.dark) :deep(.cm-lineNumbers .cm-db-result-source-line-number) {
|
||||
color: rgb(216 180 254) !important;
|
||||
}
|
||||
|
||||
:deep(.cm-db-current-statement-line) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { defaultQueryResultArchiveFileName } from "@/lib/query/queryResultArchive";
|
||||
import { saveQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
|
||||
import { isTableDataEditable } from "@/lib/table/tableEditing";
|
||||
@@ -729,6 +729,14 @@ function toggleResultAutoSave() {
|
||||
toast(t(enabled ? "tabs.autoKeepResultsEnabled" : "tabs.autoKeepResultsDisabled"), 2500);
|
||||
}
|
||||
|
||||
function selectResultItem(item: (typeof visibleResultItems.value)[number]) {
|
||||
queryStore.setActiveResultIndex(props.activeTab.id, item.index);
|
||||
emit("update:activeOutputView", "result");
|
||||
nextTick(() => {
|
||||
queryEditorRef.value?.previewStatementRange(resultSourceRange(props.activeTab.sql, item.result, item.index, activeEffectiveDatabaseType.value) ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
function handleModRTarget(target: Element): boolean {
|
||||
if (target.closest("[data-query-editor-root]")) return queryEditorRef.value?.openReplace() ?? false;
|
||||
if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false;
|
||||
@@ -852,20 +860,11 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
||||
<div class="result-tab-scrollbar__thumb" :style="resultTabsScrollbarThumbStyle" />
|
||||
</div>
|
||||
<div ref="resultTabsScrollerRef" class="result-tab-scroll flex h-full items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :style="resultTabsScrollerStyle" @scroll="updateResultTabsScrollbar" @wheel="onResultTabsWheel">
|
||||
<Button
|
||||
v-for="item in visibleResultItems"
|
||||
:key="item.index"
|
||||
size="sm"
|
||||
:variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'"
|
||||
class="h-6 max-w-48 shrink-0 overflow-hidden text-ellipsis whitespace-nowrap px-2 text-xs"
|
||||
:title="item.title || item.label || t('tabs.resultN', { n: item.n })"
|
||||
@click="
|
||||
queryStore.setActiveResultIndex(activeTab.id, item.index);
|
||||
emit('update:activeOutputView', 'result');
|
||||
"
|
||||
>
|
||||
{{ item.label || t("tabs.resultN", { n: item.n }) }}
|
||||
</Button>
|
||||
<LightTooltip v-for="item in visibleResultItems" :key="item.index" :text="item.label || item.title || t('tabs.resultN', { n: item.n })" :disabled="!item.labelTruncated && !(!item.label && item.title)" :delay="150" :close-delay="0" nowrap>
|
||||
<Button size="sm" :variant="activeOutputView === 'result' && (activeTab.activeResultIndex ?? 0) === item.index ? 'default' : 'ghost'" class="h-6 min-w-0 max-w-48 shrink-0 px-2 text-xs" :aria-label="item.label || t('tabs.resultN', { n: item.n })" @click="selectResultItem(item)">
|
||||
<span class="block min-w-0 max-w-44 whitespace-nowrap">{{ item.displayLabel || item.label || t("tabs.resultN", { n: item.n }) }}</span>
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+70
-3
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { queryResultBaseSql, queryResultExecutionSql, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import { middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, tabularResultItems } from "@/lib/tabs/tabPresentation";
|
||||
import type { QueryTab } from "@/types/database";
|
||||
|
||||
function queryTab(overrides: Partial<QueryTab>): QueryTab {
|
||||
@@ -53,7 +53,13 @@ describe("query result SQL selection", () => {
|
||||
});
|
||||
|
||||
describe("query result labels", () => {
|
||||
it("uses source labels while keeping the full statement as the title", () => {
|
||||
it("preserves both ends when shortening long source labels", () => {
|
||||
expect(middleEllipsis("easy_manager_tool.tool_monitor_data_index_item")).toBe("easy_manage...index_item");
|
||||
expect(middleEllipsis("aaa.apis")).toBe("aaa.apis");
|
||||
expect(middleEllipsis("abcdef", 4)).toBe("a...");
|
||||
});
|
||||
|
||||
it("uses the full source label as the result tab tooltip", () => {
|
||||
const [item] = tabularResultItems([
|
||||
{
|
||||
columns: ["id"],
|
||||
@@ -66,7 +72,26 @@ describe("query result labels", () => {
|
||||
]);
|
||||
|
||||
expect(item?.label).toBe("app.users");
|
||||
expect(item?.title).toBe("SELECT * FROM users");
|
||||
expect(item?.displayLabel).toBe("app.users");
|
||||
expect(item?.labelTruncated).toBe(false);
|
||||
expect(item?.title).toBe("app.users");
|
||||
});
|
||||
|
||||
it("exposes a middle-shortened display label while retaining the full tooltip", () => {
|
||||
const [item] = tabularResultItems([
|
||||
{
|
||||
columns: ["id"],
|
||||
rows: [[1]],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
sourceLabel: "easy_manager_tool.tool_monitor_data_index_item",
|
||||
sourceStatement: "SELECT * FROM tool_monitor_data_index_item",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(item?.displayLabel).toBe("easy_manage...index_item");
|
||||
expect(item?.labelTruncated).toBe(true);
|
||||
expect(item?.title).toBe("easy_manager_tool.tool_monitor_data_index_item");
|
||||
});
|
||||
|
||||
it("does not expose SQL text as a visible fallback label", () => {
|
||||
@@ -84,3 +109,45 @@ describe("query result labels", () => {
|
||||
expect(item?.title).toBe("SELECT 1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("query result source ranges", () => {
|
||||
it("uses the result index to distinguish repeated statements", () => {
|
||||
const sql = "SELECT * FROM users;\nSELECT * FROM users;";
|
||||
const range = resultSourceRange(sql, { sourceStatement: "SELECT * FROM users" }, 1, "mysql");
|
||||
|
||||
expect(range).toEqual({
|
||||
from: sql.lastIndexOf("SELECT"),
|
||||
to: sql.length - 1,
|
||||
sql: "SELECT * FROM users",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves newline-separated MongoDB commands with the Mongo shell parser", () => {
|
||||
const sql = "db.model_field_group.find({})\n\ndb.model_info.find({})";
|
||||
const sourceStatement = "db.model_info.find({})";
|
||||
const range = resultSourceRange(sql, { sourceStatement }, 1, "mongodb");
|
||||
|
||||
expect(range).toEqual({
|
||||
from: sql.indexOf(sourceStatement),
|
||||
to: sql.length,
|
||||
sql: sourceStatement,
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves newline-separated Redis commands with the Redis parser", () => {
|
||||
const sql = "GET first\n\nGET second";
|
||||
const sourceStatement = "GET second";
|
||||
const range = resultSourceRange(sql, { sourceStatement }, 1, "redis");
|
||||
|
||||
expect(range).toEqual({
|
||||
from: sql.indexOf(sourceStatement),
|
||||
to: sql.length,
|
||||
sql: sourceStatement,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not highlight a stale or ambiguous statement", () => {
|
||||
expect(resultSourceRange("SELECT * FROM users;", { sourceStatement: "SELECT * FROM orders" }, 0, "mysql")).toBeUndefined();
|
||||
expect(resultSourceRange("SELECT * FROM users; SELECT * FROM users;", { sourceStatement: "SELECT * FROM users" }, undefined, "mysql")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "@/types/database";
|
||||
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
|
||||
import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import type { ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
||||
export type OutputView = "result" | "summary" | "explain" | "chart";
|
||||
@@ -145,10 +147,41 @@ export function queryResultStatementLabel(result: Pick<QueryResult, "sourceLabel
|
||||
return result.sourceLabel;
|
||||
}
|
||||
|
||||
export function middleEllipsis(value: string, maxLength = 24): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
if (maxLength <= 3) return ".".repeat(Math.max(0, maxLength));
|
||||
const visibleLength = maxLength - 3;
|
||||
const startLength = Math.ceil(visibleLength / 2);
|
||||
const endLength = Math.floor(visibleLength / 2);
|
||||
const end = endLength > 0 ? value.slice(-endLength) : "";
|
||||
return `${value.slice(0, startLength)}...${end}`;
|
||||
}
|
||||
|
||||
export function resultSqlForGrid(tab: Pick<QueryTab, "result" | "resultBaseSql" | "lastExecutedSql" | "sql">): string {
|
||||
return tab.result?.sourceStatement || tab.resultBaseSql || tab.lastExecutedSql || tab.sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a result's executed statement back to its current editor range.
|
||||
* A stale or ambiguous source is ignored instead of highlighting a different
|
||||
* statement that happens to have the same text.
|
||||
*/
|
||||
export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "sourceStatement"> | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined {
|
||||
const sourceStatement = result?.sourceStatement;
|
||||
if (!sourceStatement) return undefined;
|
||||
|
||||
const statements = databaseType === "redis" ? executableStatementRanges(editorSql, databaseType) : databaseType === "mongodb" ? splitMongoCommandRanges(editorSql).map(({ from, to, text }) => ({ from, to, sql: text })) : splitSqlStatementRanges(editorSql, databaseType);
|
||||
const indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined;
|
||||
if (indexed?.sql === sourceStatement) {
|
||||
return { from: indexed.from, to: indexed.to, sql: indexed.sql };
|
||||
}
|
||||
|
||||
const matches = statements.filter((statement) => statement.sql === sourceStatement);
|
||||
if (matches.length !== 1) return undefined;
|
||||
const [match] = matches;
|
||||
return { from: match.from, to: match.to, sql: match.sql };
|
||||
}
|
||||
|
||||
export function queryResultBaseSql(tab: Pick<QueryTab, "result" | "resultBaseSql" | "lastExecutedSql" | "sql">): string {
|
||||
return resultSqlForGrid(tab);
|
||||
}
|
||||
@@ -157,12 +190,23 @@ export function queryResultExecutionSql(tab: Pick<QueryTab, "result" | "resultBa
|
||||
return tab.resultSortedSql || resultSqlForGrid(tab);
|
||||
}
|
||||
|
||||
export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string; title?: string }[] {
|
||||
export function tabularResultItems(results: QueryResult[] | undefined): { result: QueryResult; index: number; n: number; label?: string; displayLabel?: string; labelTruncated: boolean; title?: string }[] {
|
||||
if (!results) return [];
|
||||
return results
|
||||
.map((result, index) => ({ result, index }))
|
||||
.filter((item) => item.result.columns.length > 0)
|
||||
.map((item, ordinal) => ({ ...item, n: ordinal + 1, label: queryResultStatementLabel(item.result), title: item.result.sourceStatement }));
|
||||
.map((item, ordinal) => {
|
||||
const label = queryResultStatementLabel(item.result);
|
||||
const displayLabel = label ? middleEllipsis(label) : undefined;
|
||||
return {
|
||||
...item,
|
||||
n: ordinal + 1,
|
||||
label,
|
||||
displayLabel,
|
||||
labelTruncated: !!label && displayLabel !== label,
|
||||
title: item.result.sourceLabel || item.result.sourceStatement,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function activeResultRun(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">) {
|
||||
|
||||
+39
-2
@@ -1,7 +1,7 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { activeResultRun, databaseDisplayNameForTab, executionSummaryItems, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSqlForGrid, tabDisplayTitle, tabModeLabel, tabularResultItems } from "../../apps/desktop/src/lib/tabs/tabPresentation.ts";
|
||||
import { activeResultRun, databaseDisplayNameForTab, executionSummaryItems, middleEllipsis, nextExecutionSummaryView, resultGridCacheKey, resultRunItems, resultSourceRange, resultSqlForGrid, tabDisplayTitle, tabModeLabel, tabularResultItems } from "../../apps/desktop/src/lib/tabs/tabPresentation.ts";
|
||||
import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts";
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
@@ -184,7 +184,7 @@ test("tabular result items expose source labels when available", () => {
|
||||
assert.deepEqual(
|
||||
tabularResultItems(results).map((item) => ({ index: item.index, n: item.n, label: item.label, title: item.title })),
|
||||
[
|
||||
{ index: 1, n: 1, label: "public.users", title: "select * from public.users" },
|
||||
{ index: 1, n: 1, label: "public.users", title: "public.users" },
|
||||
{ index: 2, n: 2, label: undefined, title: "select id, name, email, created_at from users where active = true order by created_at desc" },
|
||||
],
|
||||
);
|
||||
@@ -194,6 +194,43 @@ test("tabular result items expose source labels when available", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("middleEllipsis preserves the beginning and end of long source labels", () => {
|
||||
assert.equal(middleEllipsis("easy_manager_tool.tool_monitor_data_index_item"), "easy_manage...index_item");
|
||||
assert.equal(middleEllipsis("aaa.apis"), "aaa.apis");
|
||||
});
|
||||
|
||||
test("resultSourceRange uses the result index for repeated SQL", () => {
|
||||
const sql = "select * from users;\nselect * from users;";
|
||||
assert.deepEqual(resultSourceRange(sql, { sourceStatement: "select * from users" }, 1, "mysql"), {
|
||||
from: sql.lastIndexOf("select"),
|
||||
to: sql.length - 1,
|
||||
sql: "select * from users",
|
||||
});
|
||||
assert.equal(resultSourceRange("select * from users;", { sourceStatement: "select * from orders" }, 0, "mysql"), undefined);
|
||||
});
|
||||
|
||||
test("resultSourceRange resolves newline-separated MongoDB commands", () => {
|
||||
const sql = "db.model_field_group.find({})\n\ndb.model_info.find({})";
|
||||
const sourceStatement = "db.model_info.find({})";
|
||||
|
||||
assert.deepEqual(resultSourceRange(sql, { sourceStatement }, 1, "mongodb"), {
|
||||
from: sql.indexOf(sourceStatement),
|
||||
to: sql.length,
|
||||
sql: sourceStatement,
|
||||
});
|
||||
});
|
||||
|
||||
test("resultSourceRange resolves newline-separated Redis commands", () => {
|
||||
const sql = "GET first\n\nGET second";
|
||||
const sourceStatement = "GET second";
|
||||
|
||||
assert.deepEqual(resultSourceRange(sql, { sourceStatement }, 1, "redis"), {
|
||||
from: sql.indexOf(sourceStatement),
|
||||
to: sql.length,
|
||||
sql: sourceStatement,
|
||||
});
|
||||
});
|
||||
|
||||
test("resultSqlForGrid prefers the active result source statement", () => {
|
||||
const tab = queryTab({
|
||||
sql: "select * from users; select * from orders",
|
||||
|
||||
Reference in New Issue
Block a user