feat(desktop): add database info and SQL statement markers

This commit is contained in:
shen authored and GitHub committed 2026-07-15 18:35:34 +08:00
1 parent 83f7604a11
commit 3748d83cde
60 files changed
+2403 -134

No files matched your search

+56 -1
View File
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useConnectionStore } from "@/stores/connectionStore";
import { connectionGroupDisplayName, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation";
import { connectionGroupDisplayName, middleEllipsis, queryResultBaseSql, queryResultExecutionSql, resultSourceRange, statementExecutionMarkers, tabTooltipLines, tabularResultItems } from "@/lib/tabs/tabPresentation";
import type { ConnectionConfig, QueryTab } from "@/types/database";
const translations: Record<string, string> = {
@@ -222,3 +222,58 @@ describe("query result source ranges", () => {
expect(resultSourceRange("SELECT * FROM users; SELECT * FROM users;", { sourceStatement: "SELECT * FROM users" }, undefined, "mysql")).toBeUndefined();
});
});
describe("statement execution markers", () => {
it("projects explicit statement indexes to current editor lines", () => {
const sql = "SELECT 1;\nSELECT * FROM missing;\nSELECT 3;";
const secondFrom = sql.indexOf("SELECT *");
const markers = statementExecutionMarkers(
sql,
[
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 0, sourceStatement: "SELECT 1", sourceFrom: 0, sourceTo: 8 },
{ columns: ["Error"], rows: [["no such table"]], affected_rows: 0, execution_time_ms: 1, execution_error: true, statement_index: 1, sourceStatement: "SELECT * FROM missing", sourceFrom: secondFrom, sourceTo: secondFrom + "SELECT * FROM missing".length },
],
"sqlite",
sql,
);
expect(markers).toEqual([
{ from: 0, status: "success", successCount: 1, errorCount: 0 },
{ from: secondFrom, status: "error", successCount: 0, errorCount: 1 },
]);
});
it("omits unindexed query-level errors and single-statement executions", () => {
expect(statementExecutionMarkers("SELECT 1; SELECT 2;", [{ columns: ["Error"], rows: [["pool failed"]], affected_rows: 0, execution_time_ms: 1, execution_error: true }], "mysql", "SELECT 1; SELECT 2;")).toEqual([]);
expect(statementExecutionMarkers("SELECT 1", [{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 0, sourceStatement: "SELECT 1", sourceFrom: 0, sourceTo: 8 }], "mysql", "SELECT 1")).toEqual([]);
});
it("keeps duplicate statements scoped by preserved absolute ranges", () => {
const sql = "SELECT * FROM users;\nSELECT * FROM users;";
const from = sql.lastIndexOf("SELECT");
expect(statementExecutionMarkers(sql, [{ columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 1, sourceStatement: "SELECT * FROM users", sourceFrom: from, sourceTo: sql.length - 1 }], "mysql", sql)).toEqual([
{ from, status: "success", successCount: 1, errorCount: 0 },
]);
});
it("aggregates same-line statements with error precedence", () => {
const sql = "SELECT 1; SELECT bad;";
expect(
statementExecutionMarkers(
sql,
[
{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 0, sourceStatement: "SELECT 1", sourceFrom: 0, sourceTo: 8 },
{ columns: ["Error"], rows: [["bad"]], affected_rows: 0, execution_time_ms: 1, execution_error: true, statement_index: 1, sourceStatement: "SELECT bad", sourceFrom: 10, sourceTo: 20 },
],
"mysql",
sql,
),
).toEqual([{ from: 0, status: "error", successCount: 1, errorCount: 1 }]);
});
it("invalidates every marker after the editor document changes", () => {
const executedSql = "SELECT 1;\nSELECT 2;";
expect(statementExecutionMarkers(`-- edited\n${executedSql}`, [{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1, statement_index: 0, sourceStatement: "SELECT 1" }], "mysql", "stale-editor-fingerprint", executedSql)).toEqual([]);
});
});
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { decodeTabResultSnapshot, encodeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
describe("tab result cache statement execution metadata", () => {
it("preserves statement identity and the editor fingerprint", () => {
const encoded = encodeTabResultSnapshot({
results: [
{
columns: ["value"],
rows: [[1]],
affected_rows: 0,
execution_time_ms: 1,
statement_index: 0,
sourceStatement: "SELECT 1",
},
{
columns: ["Error"],
rows: [["failed"]],
affected_rows: 0,
execution_time_ms: 1,
execution_error: true,
statement_index: 1,
sourceStatement: "SELECT bad",
},
],
resultEditorFingerprint: "15:0123456789abcdef",
cachedAt: 1,
});
const restored = decodeTabResultSnapshot(encoded);
expect(restored?.resultEditorFingerprint).toBe("15:0123456789abcdef");
expect(restored?.results?.map((result) => ({ statementIndex: result.statement_index, error: result.execution_error }))).toEqual([
{ statementIndex: 0, error: undefined },
{ statementIndex: 1, error: true },
]);
});
});
+3
View File
@@ -51,7 +51,10 @@ function forward<K extends keyof Backend>(name: K): Backend[K] {
// Connection
export const testConnection = forward("testConnection");
export const testConnectionWithInfo = forward("testConnectionWithInfo");
export const connectDb = forward("connectDb");
export const connectionDatabaseInfo = forward("connectionDatabaseInfo");
export const saveConnectionDatabaseInfo = forward("saveConnectionDatabaseInfo");
export const connectionFinalProxyPort = forward("connectionFinalProxyPort");
export const disconnectDb = forward("disconnectDb");
export const checkConnectionHealth = forward("checkConnectionHealth");
+25
View File
@@ -1,5 +1,7 @@
import type {
ConnectionConfig,
ConnectionTestResult,
DatabaseConnectionInfo,
DatabaseInfo,
SchemaInfo,
LinkedServerInfo,
@@ -150,6 +152,7 @@ import type {
NacosServiceQuery,
} from "@/types/nacos";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
// ---------------------------------------------------------------------------
// Helpers
@@ -209,10 +212,32 @@ export async function testConnection(config: ConnectionConfig): Promise<string>
return post("/api/connection/test", { config });
}
export async function testConnectionWithInfo(config: ConnectionConfig): Promise<ConnectionTestResult> {
const response = await fetch(apiUrl("/api/connection/test-info"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ config }),
});
if (response.status === 404) {
return normalizeConnectionTestResult(await testConnection(config), config);
}
if (!response.ok) throw new Error(await response.text());
return normalizeConnectionTestResult(await response.json(), config);
}
export async function connectDb(config: ConnectionConfig, clientAttempt?: number): Promise<string> {
return post("/api/connection/connect", { config, clientAttempt });
}
export async function connectionDatabaseInfo(connectionId: string, database?: string): Promise<DatabaseConnectionInfo | undefined> {
const info = await post<DatabaseConnectionInfo | null>("/api/connection/database-info", { connectionId, database });
return info ?? undefined;
}
export async function saveConnectionDatabaseInfo(connectionId: string, databaseInfo: DatabaseConnectionInfo): Promise<void> {
return post("/api/connection/database-info/save", { connectionId, databaseInfo });
}
export async function connectionFinalProxyPort(config: ConnectionConfig): Promise<number> {
return post("/api/connection/final-proxy-port", { config });
}
+22
View File
@@ -2,6 +2,8 @@ import { invoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type {
ConnectionConfig,
ConnectionTestResult,
DatabaseConnectionInfo,
DatabaseInfo,
SchemaInfo,
LinkedServerInfo,
@@ -36,6 +38,7 @@ import type {
SshConfigHostEntry,
TunnelProfile,
} from "@/types/database";
import { isTauriCommandUnavailable, normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
import type { CollectionInfo } from "@/types/database";
import type { SidebarObjectKind } from "@/lib/database/databaseObjectCapabilities";
import type { AiConfig, AiTestConnectionResult } from "@/stores/settingsStore";
@@ -636,10 +639,29 @@ export async function testConnection(config: ConnectionConfig): Promise<string>
return invoke("test_connection", { config });
}
export async function testConnectionWithInfo(config: ConnectionConfig): Promise<ConnectionTestResult> {
try {
const result = await invoke<unknown>("test_connection_with_info", { config });
return normalizeConnectionTestResult(result, config);
} catch (error) {
if (!isTauriCommandUnavailable(error, "test_connection_with_info")) throw error;
return normalizeConnectionTestResult(await testConnection(config), config);
}
}
export async function connectDb(config: ConnectionConfig, clientAttempt?: number): Promise<string> {
return invoke("connect_db", { config, clientAttempt });
}
export async function connectionDatabaseInfo(connectionId: string, database?: string): Promise<DatabaseConnectionInfo | undefined> {
const info = await invoke<DatabaseConnectionInfo | null>("connection_database_info", { connectionId, database });
return info ?? undefined;
}
export async function saveConnectionDatabaseInfo(connectionId: string, databaseInfo: DatabaseConnectionInfo): Promise<void> {
return invoke("save_connection_database_info", { connectionId, databaseInfo });
}
export async function connectionFinalProxyPort(config: ConnectionConfig): Promise<number> {
return invoke("connection_final_proxy_port", { config });
}
@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import type { ConnectionConfig } from "@/types/database";
import { connectionConfigFingerprint, databaseInfoCopyText, databaseInfoRows, isTauriCommandUnavailable, normalizeConnectionTestResult } from "@/lib/connection/connectionDatabaseInfo";
function config(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
return {
id: "connection-1",
name: "Local H2",
db_type: "h2",
driver_label: "H2",
host: "127.0.0.1",
port: 9092,
username: "sa",
password: "secret",
...overrides,
};
}
describe("connectionDatabaseInfo", () => {
it("normalizes structured and legacy responses with a configured product fallback", () => {
expect(normalizeConnectionTestResult("Connection successful", config())).toEqual({
message: "Connection successful",
databaseInfo: { productName: "H2" },
});
expect(normalizeConnectionTestResult("Connection successful", config({ database: "app" })).databaseInfo).toMatchObject({
productName: "H2",
currentDatabase: "app",
});
expect(
normalizeConnectionTestResult(
{
message: "Connection successful",
databaseInfo: {
productName: " H2 ",
productVersion: " 2.2.224 ",
currentDatabase: " testdb ",
serverCharset: " utf8mb4 ",
driverName: " ",
unquotedIdentifierCase: "UPPER",
},
},
config(),
).databaseInfo,
).toEqual({
productName: "H2",
productVersion: "2.2.224",
currentDatabase: "testdb",
serverComment: undefined,
serverCharset: "utf8mb4",
serverCollation: undefined,
unquotedIdentifierCase: "upper",
quotedIdentifierCase: undefined,
driverName: undefined,
driverVersion: undefined,
jdbcVersion: undefined,
});
});
it("fingerprints the complete submitted config without depending on object key order", () => {
const original = config({ transport_layers: [{ id: "ssh", type: "ssh", host: "jump", port: 22, user: "root", password: "hop-secret" }] });
const reordered = Object.fromEntries(Object.entries(original).reverse()) as unknown as ConnectionConfig;
expect(connectionConfigFingerprint(reordered)).toBe(connectionConfigFingerprint(original));
expect(connectionConfigFingerprint({ ...original, password: "changed" })).not.toBe(connectionConfigFingerprint(original));
expect(connectionConfigFingerprint({ ...original, name: "Renamed" })).not.toBe(connectionConfigFingerprint(original));
expect(connectionConfigFingerprint({ ...original, transport_layers: [{ ...original.transport_layers![0], host: "other-jump" }] })).not.toBe(connectionConfigFingerprint(original));
expect(connectionConfigFingerprint(original, "")).not.toBe(connectionConfigFingerprint(original, original.name));
expect(connectionConfigFingerprint({ ...original, database_info: { productName: "MySQL", productVersion: "8.4.0" } })).toBe(connectionConfigFingerprint(original));
});
it("formats only database metadata for rows and copied text", () => {
const info = normalizeConnectionTestResult(
{
message: "ok",
databaseInfo: { productName: "H2", currentDatabase: "app", driverName: "H2 JDBC Driver", jdbcVersion: "4.2" },
},
config(),
).databaseInfo!;
const copy = databaseInfoCopyText(
info,
(field) => field,
(value) => value,
);
expect(databaseInfoRows(info).map((row) => row.key)).toEqual(["productName", "currentDatabase", "driverName", "jdbcVersion"]);
expect(copy).toContain("productName: H2");
expect(copy).toContain("currentDatabase: app");
expect(copy).not.toContain("127.0.0.1");
expect(copy).not.toContain("secret");
expect(copy).not.toContain("connection_string");
});
it("recognizes only explicit missing-command errors for Tauri fallback", () => {
expect(isTauriCommandUnavailable("Command test_connection_with_info not found", "test_connection_with_info")).toBe(true);
expect(isTauriCommandUnavailable("unknown command 'test_connection_with_info'", "test_connection_with_info")).toBe(true);
expect(isTauriCommandUnavailable("Authentication failed", "test_connection_with_info")).toBe(false);
expect(isTauriCommandUnavailable("Command test_connection not found", "test_connection_with_info")).toBe(false);
expect(isTauriCommandUnavailable("Database not found while invoking command test_connection_with_info", "test_connection_with_info")).toBe(false);
});
});
@@ -0,0 +1,120 @@
import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, IdentifierCase } from "@/types/database";
export type DatabaseInfoField = keyof DatabaseConnectionInfo;
export interface DatabaseInfoRow {
key: DatabaseInfoField;
value: string;
}
const DATABASE_INFO_FIELDS: readonly DatabaseInfoField[] = ["productName", "productVersion", "currentDatabase", "serverComment", "serverCharset", "serverCollation", "unquotedIdentifierCase", "quotedIdentifierCase", "driverName", "driverVersion", "jdbcVersion"];
const IDENTIFIER_CASES = new Set<IdentifierCase>(["lower", "upper", "mixed"]);
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function nonBlankString(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed || undefined;
}
function identifierCase(value: unknown): IdentifierCase | undefined {
const normalized = nonBlankString(value)?.toLowerCase() as IdentifierCase | undefined;
return normalized && IDENTIFIER_CASES.has(normalized) ? normalized : undefined;
}
export function configuredDatabaseProductName(config: Pick<ConnectionConfig, "db_type" | "driver_label">): string {
return config.driver_label?.trim() || config.db_type;
}
export function normalizeDatabaseConnectionInfo(value: unknown, fallbackProductName?: string, fallbackCurrentDatabase?: string): DatabaseConnectionInfo | undefined {
const source = isRecord(value) ? value : {};
const result: DatabaseConnectionInfo = {
productName: nonBlankString(source.productName) ?? nonBlankString(fallbackProductName),
productVersion: nonBlankString(source.productVersion),
currentDatabase: nonBlankString(source.currentDatabase) ?? nonBlankString(fallbackCurrentDatabase),
serverComment: nonBlankString(source.serverComment),
serverCharset: nonBlankString(source.serverCharset),
serverCollation: nonBlankString(source.serverCollation),
unquotedIdentifierCase: identifierCase(source.unquotedIdentifierCase),
quotedIdentifierCase: identifierCase(source.quotedIdentifierCase),
driverName: nonBlankString(source.driverName),
driverVersion: nonBlankString(source.driverVersion),
jdbcVersion: nonBlankString(source.jdbcVersion),
};
return DATABASE_INFO_FIELDS.some((key) => result[key] !== undefined) ? result : undefined;
}
export function normalizeConnectionTestResult(value: unknown, config: ConnectionConfig): ConnectionTestResult {
const fallbackProductName = configuredDatabaseProductName(config);
const fallbackCurrentDatabase = config.database;
if (typeof value === "string") {
return {
message: value,
databaseInfo: normalizeDatabaseConnectionInfo(undefined, fallbackProductName, fallbackCurrentDatabase),
};
}
if (!isRecord(value) || typeof value.message !== "string") {
throw new Error("Invalid connection test response");
}
return {
message: value.message,
databaseInfo: normalizeDatabaseConnectionInfo(value.databaseInfo, fallbackProductName, fallbackCurrentDatabase),
};
}
function stableValue(value: unknown): unknown {
if (Array.isArray(value)) return value.map(stableValue);
if (!isRecord(value)) return value;
const result: Record<string, unknown> = {};
for (const key of Object.keys(value).sort()) {
const child = value[key];
if (child !== undefined) result[key] = stableValue(child);
}
return result;
}
function fnv1a(value: string, seed: number): number {
let hash = seed >>> 0;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
export function connectionConfigFingerprint(config: ConnectionConfig, sourceName = config.name): string {
const { database_info: _databaseInfo, ...submittedConfig } = config;
const serialized = JSON.stringify(stableValue({ config: submittedConfig, sourceName }));
const first = fnv1a(serialized, 0x811c9dc5).toString(16).padStart(8, "0");
const second = fnv1a(serialized, 0x9e3779b9).toString(16).padStart(8, "0");
return `${first}${second}`;
}
export function databaseInfoRows(info: DatabaseConnectionInfo): DatabaseInfoRow[] {
return DATABASE_INFO_FIELDS.flatMap((key) => {
const value = info[key];
return value ? [{ key, value }] : [];
});
}
export function databaseInfoSummary(info: DatabaseConnectionInfo): string {
return [info.productName, info.productVersion].filter(Boolean).join(" ");
}
export function databaseInfoCopyText(info: DatabaseConnectionInfo, fieldLabel: (field: DatabaseInfoField) => string, caseLabel: (value: IdentifierCase) => string): string {
return databaseInfoRows(info)
.map((row) => {
const value = row.key === "unquotedIdentifierCase" || row.key === "quotedIdentifierCase" ? caseLabel(row.value as IdentifierCase) : row.value;
return `${fieldLabel(row.key)}: ${value}`;
})
.join("\n");
}
export function isTauriCommandUnavailable(error: unknown, command: string): boolean {
const message = error instanceof Error ? error.message : String(error);
const escapedCommand = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`unknown command\\s*[:=]?\\s*['"]?${escapedCommand}['"]?`, "i").test(message) || new RegExp(`command\\s+['"]?${escapedCommand}['"]?\\s+(?:was\\s+)?not found`, "i").test(message);
}
@@ -0,0 +1,14 @@
function fnv1a(value: string, seed: number): number {
let hash = seed >>> 0;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
export function sqlTextFingerprint(sql: string): string {
const first = fnv1a(sql, 0x811c9dc5).toString(16).padStart(8, "0");
const second = fnv1a(sql, 0x9e3779b9).toString(16).padStart(8, "0");
return `${sql.length.toString(16)}:${first}${second}`;
}
+59 -1
View File
@@ -3,6 +3,7 @@ import { useSettingsStore } from "@/stores/settingsStore";
import { findConnectionGroupPath } from "@/lib/sidebar/sidebarLayout";
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
import { executableStatementRanges, splitSqlStatementRanges, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
import { sqlTextFingerprint } from "@/lib/sql/sqlTextFingerprint";
import type { ConnectionConfig, DatabaseType, QueryResult, QueryTab } from "@/types/database";
type Translate = (key: string, params?: Record<string, unknown>) => string;
@@ -179,7 +180,7 @@ export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "
return { from: result.sourceFrom, to: result.sourceTo, sql: sourceStatement };
}
const statements = databaseType === "redis" ? executableStatementRanges(editorSql, databaseType) : databaseType === "mongodb" ? splitMongoCommandRanges(editorSql).map(({ from, to, text }) => ({ from, to, sql: text })) : splitSqlStatementRanges(editorSql, databaseType);
const statements = statementRanges(editorSql, databaseType);
const indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined;
if (indexed?.sql === sourceStatement) {
return { from: indexed.from, to: indexed.to, sql: indexed.sql };
@@ -191,6 +192,63 @@ export function resultSourceRange(editorSql: string, result: Pick<QueryResult, "
return { from: match.from, to: match.to, sql: match.sql };
}
export type StatementExecutionMarkerStatus = "success" | "error";
export interface StatementExecutionMarker {
from: number;
status: StatementExecutionMarkerStatus;
successCount: number;
errorCount: number;
}
function lineStartOffset(sql: string, from: number): number {
return sql.lastIndexOf("\n", Math.max(0, from - 1)) + 1;
}
function statementRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
if (databaseType === "redis") return executableStatementRanges(sql, databaseType);
if (databaseType === "mongodb") return splitMongoCommandRanges(sql).map(({ from, to, text }) => ({ from, to, sql: text }));
return splitSqlStatementRanges(sql, databaseType);
}
export function statementExecutionMarkers(editorSql: string, results: QueryResult[] | undefined, databaseType?: DatabaseType, submittedSql = editorSql, executionEditorFingerprint = sqlTextFingerprint(editorSql)): StatementExecutionMarker[] {
if (!results?.length || sqlTextFingerprint(editorSql) !== executionEditorFingerprint) return [];
const submittedStatements = statementRanges(submittedSql, databaseType);
if (submittedStatements.length <= 1) return [];
const editorStatements = submittedSql === editorSql ? submittedStatements : statementRanges(editorSql, databaseType);
const byLine = new Map<number, { success: number; error: number }>();
for (const result of results) {
if (!Number.isInteger(result.statement_index) || result.statement_index! < 0) continue;
const statementIndex = result.statement_index!;
const submittedStatement = submittedStatements[statementIndex];
if (!submittedStatement || submittedStatement.sql !== result.sourceStatement) continue;
const range =
typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === result.sourceStatement
? { from: result.sourceFrom, to: result.sourceTo, sql: result.sourceStatement }
: editorStatements[statementIndex]?.sql === result.sourceStatement
? editorStatements[statementIndex]
: undefined;
if (!range) continue;
const from = lineStartOffset(editorSql, range.from);
const current = byLine.get(from) ?? { success: 0, error: 0 };
if (result.execution_error === true) current.error += 1;
else current.success += 1;
byLine.set(from, current);
}
return [...byLine.entries()]
.sort(([a], [b]) => a - b)
.map(([from, counts]) => {
return {
from,
status: counts.error > 0 ? "error" : "success",
successCount: counts.success,
errorCount: counts.error,
};
});
}
export function queryResultBaseSql(tab: Pick<QueryTab, "result" | "resultBaseSql" | "lastExecutedSql" | "sql">): string {
return resultSqlForGrid(tab);
}
@@ -16,6 +16,7 @@ export interface TabResultSnapshot {
result?: QueryResult;
results?: QueryResult[];
activeResultIndex?: number;
resultEditorFingerprint?: string;
/**
* Source ordering retained while a local grid sort is active. It must travel
* with the snapshot so clearing the sort after a cache/archive restore can
@@ -41,6 +42,7 @@ export interface TabResultSnapshot {
interface ColumnarQueryResult {
columns: string[];
execution_error?: true;
statement_index?: number;
column_types?: string[];
columnValues: CellValue[][];
rowCount: number;
@@ -138,6 +140,7 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin
return {
columns: [...result.columns],
execution_error: result.execution_error,
statement_index: result.statement_index,
column_types: result.column_types ? [...result.column_types] : undefined,
rows: result.rows.map((row) => [...row]),
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
@@ -174,6 +177,7 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult
return removeUndefinedFields({
columns: [...result.columns],
execution_error: result.execution_error,
statement_index: result.statement_index,
column_types: result.column_types ? [...result.column_types] : undefined,
columnValues,
rowCount: result.rows.length,
@@ -195,6 +199,7 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul
return {
columns: [...result.columns],
execution_error: result.execution_error,
statement_index: result.statement_index,
column_types: result.column_types ? [...result.column_types] : undefined,
rows,
mongo_documents: result.mongo_documents ? clonePlain(result.mongo_documents) : undefined,
@@ -389,6 +394,7 @@ export function buildTabResultSnapshot(tab: QueryTab): TabResultSnapshot | undef
result: stripSessionIds(tab.result),
results: stripResultSessionIds(tab.results),
activeResultIndex: tab.activeResultIndex,
resultEditorFingerprint: tab.resultEditorFingerprint,
resultLocalSortOriginalRows: tab.resultLocalSortOriginalRows?.map((row) => [...row]),
resultLocalSortOriginalMongoDocuments: tab.resultLocalSortOriginalMongoDocuments ? clonePlain(tab.resultLocalSortOriginalMongoDocuments) : undefined,
resultRuns: stripResultRunSessionIds(tab.resultRuns),