feat(query): show result source ranges
This commit is contained in:
5 files changed
+248
-26
No files matched your search
+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">) {
|
||||
|
||||
Reference in new issue
Block a user