feat(query): show result source ranges

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-07-13 18:52:14 +08:00
1 parent d1cce3ed1e
commit 6f5dffb279
5 files changed
+248 -26

No files matched your search

+70 -3
View File
@@ -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();
});
});
+47 -3
View File
@@ -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">) {