diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index a716f7b597..6f90d2f26f 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -1109,10 +1109,13 @@ function executableSqlFromView(currentView: EditorViewType): string { } function sqlExecutionSnapshotFromView(currentView: EditorViewType): SqlExecutionSnapshot { + const selection = currentView.state.selection.main; return { fullSql: currentView.state.doc.toString(), selectedSql: selectedSqlFromView(currentView), - cursorPos: currentView.state.selection.main.head, + cursorPos: selection.head, + selectionFrom: selection.from, + selectionTo: selection.to, }; } diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts index ae215a8345..b4b545726a 100644 --- a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -109,6 +109,32 @@ describe("useSqlExecution", () => { setActivePinia(createPinia()); }); + it("passes the selected statement's editor offset to the query store", async () => { + const fullSql = "SELECT * FROM users;\nSELECT * FROM users;"; + const selectionFrom = fullSql.lastIndexOf("SELECT"); + const selectedSql = fullSql.slice(selectionFrom, -1); + const activeTab = ref({ ...queryTab("app"), sql: fullSql }); + const activeConnection = ref(connection("mysql")); + const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); + const queryStore = useQueryStore(); + const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => { + if (activeTab.value) activeTab.value.result = { columns: ["id"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }; + }); + vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); + + const execution = useSqlExecution({ + activeTab: computed(() => activeTab.value), + activeConnection: computed(() => activeConnection.value), + executableSql: computed(() => selectedSql), + resolveExecutableSql: async () => selectedSql, + activeOutputView, + }); + + await execution.tryExecute({ fullSql, selectedSql, cursorPos: selectionFrom, selectionFrom, selectionTo: fullSql.length - 1 }); + + expect(executeCurrentSql).toHaveBeenCalledWith(selectedSql, { sourceOffset: selectionFrom }); + }); + it("sends native SET variables without client-side expansion", async () => { const activeTab = ref(queryTab("app")); const activeConnection = ref(connection("mysql")); diff --git a/apps/desktop/src/composables/useSqlExecution.ts b/apps/desktop/src/composables/useSqlExecution.ts index 5fad05fda6..9c382b7713 100644 --- a/apps/desktop/src/composables/useSqlExecution.ts +++ b/apps/desktop/src/composables/useSqlExecution.ts @@ -79,29 +79,34 @@ export function useSqlExecution(deps: { const sqlParameterNames = ref([]); const sqlParameterDatabaseType = ref(); const sqlParameterEnabledSyntaxes = ref([]); + const pendingSourceOffset = ref(); - async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise { + async function resolvedExecutableSql(source?: SqlExecutionOverride): Promise<{ sql: string; sourceOffset?: number }> { const atSetEnabled = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, deps.activeConnection.value?.db_type).atSet; const expand = (sql: string) => (atSetEnabled ? expandSqlVariables(sql).sql : sql); - if (typeof source === "string") return expand(source); - if (deps.resolveExecutableSql) return expand(await deps.resolveExecutableSql(source)); - if (isSqlExecutionSnapshot(source)) return expand(resolveExecutableSql(source.fullSql, source.selectedSql, { cursorPos: source.cursorPos })); - return expand(deps.executableSql.value); + if (typeof source === "string") return { sql: expand(source) }; + + const resolved = deps.resolveExecutableSql ? await deps.resolveExecutableSql(source) : isSqlExecutionSnapshot(source) ? resolveExecutableSql(source.fullSql, source.selectedSql, { cursorPos: source.cursorPos }) : deps.executableSql.value; + const sql = expand(resolved); + if (!isSqlExecutionSnapshot(source) || !source.selectedSql.trim() || sql !== resolved) return { sql }; + + const leadingWhitespace = source.selectedSql.length - source.selectedSql.trimStart().length; + return { sql, sourceOffset: source.selectionFrom + leadingWhitespace }; } async function tryExecute(sqlOverride?: SqlExecutionOverride) { const tab = deps.activeTab.value; - const sql = await resolvedExecutableSql(sqlOverride); + const { sql, sourceOffset } = await resolvedExecutableSql(sqlOverride); if (!tab || !sql.trim()) return; if (requiresDatabaseSelection(tab, deps.activeConnection.value, sql)) { deps.onMissingDatabase?.(); return; } - if (supportsSqlTemplateParameters(deps.activeConnection.value) && prepareSqlParameterDialog(sql)) return; - await continueExecute(sql); + if (supportsSqlTemplateParameters(deps.activeConnection.value) && prepareSqlParameterDialog(sql, sourceOffset)) return; + await continueExecute(sql, sourceOffset); } - async function continueExecute(sql: string) { + async function continueExecute(sql: string, sourceOffset?: number) { // Redis: block dangerous commands when toggle is on (check each line for multi-line input) if (deps.activeConnection.value?.db_type === "redis" && deps.blockDangerousRedisCommands?.value !== false) { const commands = sql @@ -126,20 +131,21 @@ export function useSqlExecution(deps: { productionDatabases: productionAssessment.databases, source: t("production.sourceSqlEditor"), }); - if (confirmed) await doExecute(sql); + if (confirmed) await doExecute(sql, sourceOffset); return; } if (isDangerousSql(sql) && settingsStore.editorSettings.confirmDangerousSqlExecution) { dangerSql.value = sql; pendingDangerSql.value = sql; + pendingSourceOffset.value = sourceOffset; suppressDangerConfirm.value = false; showDangerDialog.value = true; } else { - await doExecute(sql); + await doExecute(sql, sourceOffset); } } - function prepareSqlParameterDialog(sql: string): boolean { + function prepareSqlParameterDialog(sql: string, sourceOffset?: number): boolean { const databaseType = deps.activeConnection.value?.db_type; const toggles = resolveSqlVariableSyntaxToggles(settingsStore.editorSettings.sqlVariableSyntaxOverrides, databaseType); const enabledSyntaxes = enabledSqlParameterSyntaxes(toggles); @@ -149,12 +155,13 @@ export function useSqlExecution(deps: { sqlParameterNames.value = parameters; sqlParameterDatabaseType.value = databaseType; sqlParameterEnabledSyntaxes.value = enabledSyntaxes; + pendingSourceOffset.value = sourceOffset; showSqlParameterDialog.value = true; return true; } - async function doExecute(sql?: string) { - sql ??= await resolvedExecutableSql(); + async function doExecute(sql?: string, sourceOffset?: number) { + if (sql === undefined) ({ sql, sourceOffset } = await resolvedExecutableSql()); const tab = deps.activeTab.value; if (!tab || !sql.trim()) return; const executionConnection = connectionStore.getConfig(tab.connectionId) ?? deps.activeConnection.value; @@ -167,7 +174,10 @@ export function useSqlExecution(deps: { const connName = executionConnection?.name || ""; const start = Date.now(); const isRedis = executionDatabaseType === "redis"; - await queryStore.executeCurrentSql(sql, isRedis ? { skipRedisSafetyCheck: deps.blockDangerousRedisCommands?.value === false } : undefined); + await queryStore.executeCurrentSql(sql, { + ...(isRedis ? { skipRedisSafetyCheck: deps.blockDangerousRedisCommands?.value === false } : {}), + ...(sourceOffset !== undefined ? { sourceOffset } : {}), + }); if (tab.result && !tab.result.columns.length && !tab.results?.some((result) => result.columns.length > 0)) { deps.activeOutputView.value = "summary"; } @@ -211,7 +221,7 @@ export function useSqlExecution(deps: { async function tryExplain(sqlOverride?: SqlExecutionOverride) { const tab = deps.activeTab.value; - const sql = await resolvedExecutableSql(sqlOverride); + const { sql } = await resolvedExecutableSql(sqlOverride); if (!tab || !sql.trim()) { toast(t("explain.emptySql")); return; @@ -229,13 +239,14 @@ export function useSqlExecution(deps: { } async function onDangerConfirm() { - const sql = pendingDangerSql.value || (await resolvedExecutableSql()); + const resolved = pendingDangerSql.value ? { sql: pendingDangerSql.value, sourceOffset: pendingSourceOffset.value } : await resolvedExecutableSql(); if (suppressDangerConfirm.value) { settingsStore.updateEditorSettings({ confirmDangerousSqlExecution: false }); } suppressDangerConfirm.value = false; pendingDangerSql.value = ""; - await doExecute(sql); + pendingSourceOffset.value = undefined; + await doExecute(resolved.sql, resolved.sourceOffset); } async function onSqlParametersConfirm(sql: string) { @@ -244,7 +255,9 @@ export function useSqlExecution(deps: { sqlParameterNames.value = []; sqlParameterDatabaseType.value = undefined; sqlParameterEnabledSyntaxes.value = []; - await continueExecute(sql); + const sourceOffset = pendingSourceOffset.value; + pendingSourceOffset.value = undefined; + await continueExecute(sql, sourceOffset); } watch(showSqlParameterDialog, (open) => { @@ -253,6 +266,7 @@ export function useSqlExecution(deps: { sqlParameterNames.value = []; sqlParameterDatabaseType.value = undefined; sqlParameterEnabledSyntaxes.value = []; + pendingSourceOffset.value = undefined; }); return { diff --git a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts index 73bfca7ab7..9f9f6b2893 100644 --- a/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts +++ b/apps/desktop/src/lib/__tests__/tabs/tabPresentation.spec.ts @@ -111,6 +111,17 @@ describe("query result labels", () => { }); describe("query result source ranges", () => { + it("prefers the preserved editor range for a selected duplicate statement", () => { + const sql = "SELECT * FROM users;\nSELECT * FROM users;"; + const from = sql.lastIndexOf("SELECT"); + + expect(resultSourceRange(sql, { sourceStatement: "SELECT * FROM users", sourceFrom: from, sourceTo: sql.length - 1 }, 0, "mysql")).toEqual({ + from, + to: sql.length - 1, + sql: "SELECT * FROM users", + }); + }); + 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"); diff --git a/apps/desktop/src/lib/sql/sqlExecutionTarget.ts b/apps/desktop/src/lib/sql/sqlExecutionTarget.ts index dc105b3cc3..5d654100db 100644 --- a/apps/desktop/src/lib/sql/sqlExecutionTarget.ts +++ b/apps/desktop/src/lib/sql/sqlExecutionTarget.ts @@ -8,6 +8,8 @@ export interface SqlExecutionSnapshot { fullSql: string; selectedSql: string; cursorPos: number; + selectionFrom: number; + selectionTo: number; } export type SqlExecutionOverride = string | SqlExecutionSnapshot; @@ -35,7 +37,7 @@ export interface SqlExecutionChoiceRequest { } export function isSqlExecutionSnapshot(value: SqlExecutionOverride | undefined): value is SqlExecutionSnapshot { - return typeof value === "object" && value !== null && typeof value.fullSql === "string" && typeof value.selectedSql === "string" && typeof value.cursorPos === "number"; + return typeof value === "object" && value !== null && typeof value.fullSql === "string" && typeof value.selectedSql === "string" && typeof value.cursorPos === "number" && typeof value.selectionFrom === "number" && typeof value.selectionTo === "number"; } export function resolveExecutableSql(fullSql: string, selectedSql: string, options?: { mode?: ExecuteMode; cursorPos?: number }): string { diff --git a/apps/desktop/src/lib/tabs/tabPresentation.ts b/apps/desktop/src/lib/tabs/tabPresentation.ts index d2b2934ccd..6f72a6af7a 100644 --- a/apps/desktop/src/lib/tabs/tabPresentation.ts +++ b/apps/desktop/src/lib/tabs/tabPresentation.ts @@ -166,9 +166,12 @@ export function resultSqlForGrid(tab: Pick | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined { +export function resultSourceRange(editorSql: string, result: Pick | undefined, resultIndex: number | undefined, databaseType?: DatabaseType): SqlTextRange | undefined { const sourceStatement = result?.sourceStatement; if (!sourceStatement) return undefined; + if (typeof result.sourceFrom === "number" && typeof result.sourceTo === "number" && editorSql.slice(result.sourceFrom, result.sourceTo) === sourceStatement) { + 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 indexed = typeof resultIndex === "number" ? statements[resultIndex] : undefined; diff --git a/apps/desktop/src/lib/tabs/tabResultCache.ts b/apps/desktop/src/lib/tabs/tabResultCache.ts index 6c62e2d68f..2a8607525e 100644 --- a/apps/desktop/src/lib/tabs/tabResultCache.ts +++ b/apps/desktop/src/lib/tabs/tabResultCache.ts @@ -51,6 +51,8 @@ interface ColumnarQueryResult { has_more?: boolean; sourceLabel?: string; sourceStatement?: string; + sourceFrom?: number; + sourceTo?: number; } type QueryResultRunSnapshot = NonNullable[number]; @@ -146,6 +148,8 @@ function stripSessionIds(result: QueryResult | undefined): QueryResult | undefin has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, + sourceFrom: result.sourceFrom, + sourceTo: result.sourceTo, }; } @@ -180,6 +184,8 @@ function toColumnarResult(result: QueryResult | undefined): ColumnarQueryResult has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, + sourceFrom: result.sourceFrom, + sourceTo: result.sourceTo, }); } @@ -199,6 +205,8 @@ function fromColumnarResult(result: ColumnarQueryResult | undefined): QueryResul has_more: result.has_more, sourceLabel: result.sourceLabel, sourceStatement: result.sourceStatement, + sourceFrom: result.sourceFrom, + sourceTo: result.sourceTo, }; } diff --git a/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts index bb758e790d..309b27af5a 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.multiStatementError.spec.ts @@ -83,6 +83,22 @@ describe("queryStore multi-statement errors", () => { expect(tab.result?.columns).toEqual(["Error"]); }); + it("preserves the selected statement's absolute editor range", async () => { + mocks.executeMulti.mockResolvedValue([{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]); + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("mysql-1", "app", "Query"); + const selectedSql = "SELECT * FROM users"; + + await store.executeTabSql(tabId, selectedSql, { sourceOffset: 21 }); + + expect(store.tabs.find((item) => item.id === tabId)?.result).toMatchObject({ + sourceStatement: selectedSql, + sourceFrom: 21, + sourceTo: 40, + }); + }); + it("does not promote an unmarked Error alias without type metadata as a batch failure", async () => { mocks.executeMulti.mockResolvedValue([ { columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }, diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index f9b876997e..03895fe8f5 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -40,7 +40,7 @@ import { frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@ import { queryResultSourceLabel } from "@/lib/sql/queryResultSource"; import { sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort"; import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; -import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; +import { executableStatementRanges, splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen"; import { clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor"; import { buildTabResultSnapshot, deleteTabResultSnapshot, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache"; @@ -137,19 +137,23 @@ function preservedResultIndex(results: QueryResult[], currentIndex: number | und return currentIndex; } -function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType): QueryResult[] { +function annotateQueryResultSources(results: QueryResult[], sql: string, database: string | undefined, databaseType?: DatabaseType, sourceOffset?: number): QueryResult[] { const statements = splitSqlStatementRanges(sql, databaseType); let statementIndex = 0; for (const result of results) { const statement = statements[statementIndex++]; if (!statement) continue; - annotateQueryResultSource(result, statement.sql, database, databaseType); + annotateQueryResultSource(result, statement.sql, database, databaseType, sourceOffset === undefined ? undefined : { from: sourceOffset + statement.from, to: sourceOffset + statement.to }); } return results; } -function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string, databaseType?: DatabaseType): QueryResult { +function annotateQueryResultSource(result: QueryResult, sourceStatement: string, database?: string, databaseType?: DatabaseType, sourceRange?: { from: number; to: number }): QueryResult { result.sourceStatement = sourceStatement; + if (sourceRange) { + result.sourceFrom = sourceRange.from; + result.sourceTo = sourceRange.to; + } const label = databaseType ? queryResultSourceLabel(sourceStatement, { database, databaseType }) : undefined; if (label) result.sourceLabel = label; return result; @@ -2069,7 +2073,7 @@ export const useQueryStore = defineStore("query", () => { await executeCurrentSql(tab.sql); } - async function executeCurrentSql(sql: string, options?: { skipRedisSafetyCheck?: boolean }) { + async function executeCurrentSql(sql: string, options?: { skipRedisSafetyCheck?: boolean; sourceOffset?: number }) { if (!activeTabId.value) return; await executeTabSql(activeTabId.value, sql, { resultBaseSql: sql, resultSortedSql: undefined, ...options }); } @@ -2485,6 +2489,7 @@ export const useQueryStore = defineStore("query", () => { preserveActiveResultIndex?: boolean; replaceActiveResultInGroup?: boolean; skipRedisSafetyCheck?: boolean; + sourceOffset?: number; sourceTraceId?: string; skipEnsureConnected?: boolean; }, @@ -2578,12 +2583,15 @@ export const useQueryStore = defineStore("query", () => { console.info("[DBX][executeTabSql:redis:start]", { traceId, db: currentDb, commandCount: commands.length, sql }); const allResults: QueryResult[] = []; + const commandRanges = executableStatementRanges(sql, "redis"); const skipSafety = options?.skipRedisSafetyCheck; let hadMutatingCommand = false; - for (const command of commands) { + for (const [commandIndex, command] of commands.entries()) { + 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(tab.connectionId, currentDb, command, skipSafety); - allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, result.command), command))); + allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, result.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 @@ -2593,7 +2601,7 @@ export const useQueryStore = defineStore("query", () => { connStore.invalidateCompletionCache(tab.connectionId, String(currentDb)); } } catch (e: any) { - allResults.push(annotateQueryResultSource({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }, command)); + allResults.push(annotateQueryResultSource({ columns: ["Error"], rows: [[e?.message ?? String(e)]], affected_rows: 0, execution_time_ms: 0 }, command, undefined, undefined, sourceRange)); } } console.info("[DBX][executeTabSql:redis:done]", { traceId, commandCount: commands.length, elapsed: elapsed() }); @@ -2647,9 +2655,10 @@ export const useQueryStore = defineStore("query", () => { for (const parsedCommand of mongoCommands) { const mongoCommand = parsedCommand.command; 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 annotateMongoResult = (result: QueryResult): QueryResult => { - const annotated = annotateQueryResultSource(result, sourceStatement); + const annotated = annotateQueryResultSource(result, sourceStatement, undefined, undefined, sourceRange); if ("collection" in mongoCommand) { annotated.sourceLabel = currentDatabase ? `${currentDatabase}.${mongoCommand.collection}` : mongoCommand.collection; } @@ -2940,7 +2949,7 @@ export const useQueryStore = defineStore("query", () => { }); executionPromise = api.executeMulti(tab.connectionId, executionDatabase, sqlToExecute, executionSchema, executionId, executionOptions); } - const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType); + const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset); if (hiddenPrimaryKeys.length > 0 && results.length === 1) { const hiddenIndexes = hiddenResultColumnIndexes(results[0]!.columns, hiddenPrimaryKeys); if (hiddenIndexes.length > 0) results[0]!.hidden_column_indexes = hiddenIndexes; diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 4c2b04d0d2..e15bdbebc6 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -497,6 +497,9 @@ export interface QueryResult { has_more?: boolean; sourceLabel?: string; sourceStatement?: string; + /** Absolute offsets in the editor document at execution time. */ + sourceFrom?: number; + sourceTo?: number; } export interface QueryResultRun {